@shop-os/modelkit 0.3.8 → 0.4.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/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
@@ -222,35 +222,43 @@ var IMAGE_MODELS = [
222
222
  GEMINI_PROMPT_MAX_CHARS,
223
223
  NANO_BANANA_2_1_RATIOS
224
224
  ),
225
- geminiImage(
226
- "gemini-3-pro-image-preview",
227
- "Nano Banana Pro",
228
- ["nb-pro"],
229
- ["1K", "2K", "4K"],
230
- {
231
- provider: "google",
232
- description: "Create stunning, production-quality visuals with exceptional realism, detail, and typography. Best for campaigns, e-commerce, product photography, and hero creatives.",
233
- logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
234
- },
235
- ["cowork", "space_builder"],
236
- { images: 14 },
237
- GEMINI_PROMPT_MAX_CHARS
238
- ),
239
- geminiImage(
240
- "gemini-3.1-flash-image-preview",
241
- "Nano Banana 2",
242
- ["nb-2"],
243
- // Flash 3.1 is the only Gemini image model with the 512 tier.
244
- ["512", "1K", "2K", "4K"],
245
- {
246
- provider: "google",
247
- description: "Generate high-quality images at speed. Perfect for brainstorming, concept boards, concept testing, creative exploration, and large batch workflows.",
248
- logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
249
- },
250
- ["cowork", "space_builder"],
251
- { images: 14 },
252
- GEMINI_PROMPT_MAX_CHARS
253
- ),
225
+ // Google retired the -preview endpoints for the GA ids; the catalog ids stay because they
226
+ // are pricing keys and stored model ids.
227
+ {
228
+ ...geminiImage(
229
+ "gemini-3-pro-image-preview",
230
+ "Nano Banana Pro",
231
+ ["nb-pro"],
232
+ ["1K", "2K", "4K"],
233
+ {
234
+ provider: "google",
235
+ description: "Create stunning, production-quality visuals with exceptional realism, detail, and typography. Best for campaigns, e-commerce, product photography, and hero creatives.",
236
+ logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
237
+ },
238
+ ["cowork", "space_builder"],
239
+ { images: 14 },
240
+ GEMINI_PROMPT_MAX_CHARS
241
+ ),
242
+ providerModelId: "gemini-3-pro-image"
243
+ },
244
+ {
245
+ ...geminiImage(
246
+ "gemini-3.1-flash-image-preview",
247
+ "Nano Banana 2",
248
+ ["nb-2"],
249
+ // Flash 3.1 is the only Gemini image model with the 512 tier.
250
+ ["512", "1K", "2K", "4K"],
251
+ {
252
+ provider: "google",
253
+ description: "Generate high-quality images at speed. Perfect for brainstorming, concept boards, concept testing, creative exploration, and large batch workflows.",
254
+ logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
255
+ },
256
+ ["cowork", "space_builder"],
257
+ { images: 14 },
258
+ GEMINI_PROMPT_MAX_CHARS
259
+ ),
260
+ providerModelId: "gemini-3.1-flash-image"
261
+ },
254
262
  geminiImage(
255
263
  "gemini-3.1-flash-lite-image",
256
264
  "Nano Banana Lite",
@@ -1384,6 +1392,7 @@ var OMNI_PROMPT = { referenceSyntax: "caption", maxChars: 5e4 };
1384
1392
  var BYTEDANCE_LOGO = "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/3bba5d81-4e57-4e71-911b-cdfa05acca2c_compressed.png";
1385
1393
  var GOOGLE_LOGO2 = "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png";
1386
1394
  var VIDEO_MODELS = [
1395
+ // Google shuts down the Veo 3.1 previews and Omni Flash 1.0 on 2026-10-22.
1387
1396
  {
1388
1397
  id: "veo-3.1",
1389
1398
  aliases: [
@@ -1396,8 +1405,9 @@ var VIDEO_MODELS = [
1396
1405
  type: "video",
1397
1406
  adapter: "google-genai",
1398
1407
  providerModelId: "veo-3.1-generate-preview",
1399
- lifecycle: "active",
1400
- exposure: ["cowork", "space_builder"],
1408
+ lifecycle: "deprecated",
1409
+ replacedBy: "gemini-omni-1.1-flash",
1410
+ exposure: [],
1401
1411
  constraints: {
1402
1412
  aspectRatios: [...OMNI_RATIOS],
1403
1413
  resolutions: ["720p", "1080p", "4k"],
@@ -1432,7 +1442,8 @@ var VIDEO_MODELS = [
1432
1442
  type: "video",
1433
1443
  adapter: "google-genai",
1434
1444
  providerModelId: "veo-3.1-fast-generate-preview",
1435
- lifecycle: "active",
1445
+ lifecycle: "deprecated",
1446
+ replacedBy: "gemini-omni-1.1-flash",
1436
1447
  exposure: [],
1437
1448
  constraints: {
1438
1449
  aspectRatios: [...OMNI_RATIOS],
@@ -1462,7 +1473,8 @@ var VIDEO_MODELS = [
1462
1473
  type: "video",
1463
1474
  adapter: "google-genai",
1464
1475
  providerModelId: "veo-3.1-lite-generate-preview",
1465
- lifecycle: "active",
1476
+ lifecycle: "deprecated",
1477
+ replacedBy: "gemini-omni-1.1-flash",
1466
1478
  exposure: [],
1467
1479
  constraints: {
1468
1480
  aspectRatios: [...OMNI_RATIOS],
@@ -1493,8 +1505,9 @@ var VIDEO_MODELS = [
1493
1505
  type: "video",
1494
1506
  adapter: "google-genai",
1495
1507
  providerModelId: "gemini-omni-flash-preview",
1496
- lifecycle: "active",
1497
- exposure: ["cowork", "space_builder"],
1508
+ lifecycle: "deprecated",
1509
+ replacedBy: "gemini-omni-1.1-flash",
1510
+ exposure: [],
1498
1511
  constraints: {
1499
1512
  // 1.0 rejects `resolution`; it renders 720p. No defaults: the adapter
1500
1513
  // sends ratio and duration only when the caller set them, so a
@@ -2049,7 +2062,8 @@ var VIDEO_MODELS = [
2049
2062
  },
2050
2063
  defaults: {},
2051
2064
  usage: { ...VIDEO_USAGE },
2052
- pricingKind: "video_per_second",
2065
+ // Higgsfield bills DoP per generation: a fixed-length clip with no duration knob.
2066
+ pricingKind: "flat",
2053
2067
  display: {
2054
2068
  provider: "higgsfield",
2055
2069
  description: "Higgsfield DOP image-to-video model; animates a required first-frame image from a text prompt with an optional last-frame image and motion controls."
@@ -3285,6 +3299,36 @@ function describeThrown(cause) {
3285
3299
 
3286
3300
  // src/retry.ts
3287
3301
  var textBody = z6.string();
3302
+ var BILLING_CODES = /* @__PURE__ */ new Set([
3303
+ "insufficient_quota",
3304
+ "billing_hard_limit_reached",
3305
+ "billing_not_active",
3306
+ "payment_required",
3307
+ "insufficient_balance",
3308
+ "insufficient_credits",
3309
+ "insufficient_credit"
3310
+ ]);
3311
+ var errorCodes = z6.object({
3312
+ code: z6.string().optional(),
3313
+ type: z6.string().optional(),
3314
+ error: z6.object({ code: z6.string().optional(), type: z6.string().optional() }).optional()
3315
+ });
3316
+ var BILLING_TEXT = /insufficient_quota|payment[ _]required|exhausted balance|insufficient (?:credits?|balance|funds)|billing_hard_limit/i;
3317
+ var BILLING_TEXT_STATUSES = /* @__PURE__ */ new Set([403, 429]);
3318
+ function billingCode(body) {
3319
+ const parsed = errorCodes.safeParse(body);
3320
+ if (!parsed.success) return false;
3321
+ const { code, type, error } = parsed.data;
3322
+ return [code, type, error?.code, error?.type].some(
3323
+ (value) => value !== void 0 && BILLING_CODES.has(value.toLowerCase())
3324
+ );
3325
+ }
3326
+ function isBillingFailure(status, body, text2) {
3327
+ if (status === 402) return true;
3328
+ if (status === void 0 || status < 400 || status >= 500) return false;
3329
+ if (billingCode(body)) return true;
3330
+ return BILLING_TEXT_STATUSES.has(status) && BILLING_TEXT.test(text2);
3331
+ }
3288
3332
  function classifyHttpError(status, body, provider, model) {
3289
3333
  const asText = textBody.safeParse(body);
3290
3334
  const text2 = asText.success ? asText.data : JSON.stringify(body);
@@ -3293,6 +3337,7 @@ function classifyHttpError(status, body, provider, model) {
3293
3337
  );
3294
3338
  let code = "provider_rejected";
3295
3339
  if (blocked) code = "content_blocked";
3340
+ else if (isBillingFailure(status, body, text2)) code = "provider_billing";
3296
3341
  else if (status === 401 || status === 403) code = "auth";
3297
3342
  else if (status === 429) code = "rate_limited";
3298
3343
  else if (status === 408 || status === 504) code = "timeout";
@@ -3355,12 +3400,11 @@ function sleep(ms, signal) {
3355
3400
  rejectAborted();
3356
3401
  return;
3357
3402
  }
3358
- let timer;
3359
3403
  const onAbort = () => {
3360
- if (timer !== void 0) clearTimeout(timer);
3404
+ clearTimeout(timer);
3361
3405
  rejectAborted();
3362
3406
  };
3363
- timer = setTimeout(() => {
3407
+ const timer = setTimeout(() => {
3364
3408
  signal?.removeEventListener("abort", onAbort);
3365
3409
  resolve();
3366
3410
  }, ms);
@@ -9150,6 +9194,13 @@ function createModelKit(config, adapters) {
9150
9194
  model: model.id
9151
9195
  });
9152
9196
  }
9197
+ function timed(event, startedAt) {
9198
+ if (event.latencyMs !== void 0) return event;
9199
+ return { ...event, latencyMs: Math.max(0, Date.now() - startedAt) };
9200
+ }
9201
+ function textTimed(event, stepCount, startedAt) {
9202
+ return stepCount === 1 ? timed(event, startedAt) : event;
9203
+ }
9153
9204
  async function emit(event) {
9154
9205
  try {
9155
9206
  await config.onUsage?.(event);
@@ -9233,17 +9284,21 @@ function createModelKit(config, adapters) {
9233
9284
  const abortSignal = combineSignals(req.abortSignal, call.signal);
9234
9285
  if (abortSignal) options.abortSignal = abortSignal;
9235
9286
  if (call.maxRetries !== void 0) options.maxRetries = call.maxRetries;
9287
+ const startedAt = Date.now();
9236
9288
  const result = await aiGenerateImage(options);
9237
9289
  const images = result.images.map(
9238
9290
  (file) => toGeneratedImage({ bytes: file.uint8Array, mime: file.mediaType })
9239
9291
  );
9240
- const usage2 = {
9241
- model: model.modelId,
9242
- provider: model.provider,
9243
- modality: "IMAGE",
9244
- unit: "images",
9245
- quantity: images.length
9246
- };
9292
+ const usage2 = timed(
9293
+ {
9294
+ model: model.modelId,
9295
+ provider: model.provider,
9296
+ modality: "IMAGE",
9297
+ unit: "images",
9298
+ quantity: images.length
9299
+ },
9300
+ startedAt
9301
+ );
9247
9302
  await emit(usage2);
9248
9303
  return { image: images[0], images, usage: usage2, raw: result, warnings };
9249
9304
  }
@@ -9316,18 +9371,22 @@ function createModelKit(config, adapters) {
9316
9371
  if (providerOptions) options.providerOptions = providerOptions;
9317
9372
  if (abortSignal) options.abortSignal = abortSignal;
9318
9373
  logCoercions(logger, model.modelId, warnings);
9374
+ const startedAt = Date.now();
9319
9375
  const pending = aiGenerateVideo(options);
9320
9376
  let settled;
9321
9377
  let failed = false;
9322
9378
  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
- };
9379
+ const usage2 = timed(
9380
+ {
9381
+ model: model.modelId,
9382
+ provider: model.provider,
9383
+ modality: "VIDEO",
9384
+ unit: "seconds",
9385
+ quantity: seconds2 ?? 0,
9386
+ ...seconds2 !== void 0 && { durationSeconds: seconds2 }
9387
+ },
9388
+ startedAt
9389
+ );
9331
9390
  settled = {
9332
9391
  video: { bytes: result.video.uint8Array, mime: result.video.mediaType },
9333
9392
  usage: usage2,
@@ -9384,8 +9443,12 @@ function createModelKit(config, adapters) {
9384
9443
  message: `${adapter.id} cannot generate images`,
9385
9444
  model: model.id
9386
9445
  });
9446
+ const startedAt = Date.now();
9387
9447
  const result = await adapter.generateImage(request, ctx);
9388
- const usage2 = { ...result.usage, model: model.id, unit: model.usage.unit };
9448
+ const usage2 = timed(
9449
+ { ...result.usage, model: model.id, unit: model.usage.unit },
9450
+ startedAt
9451
+ );
9389
9452
  await emit(usage2);
9390
9453
  const images = result.images.map(toGeneratedImage);
9391
9454
  const first = images[0];
@@ -9428,6 +9491,7 @@ function createModelKit(config, adapters) {
9428
9491
  message: `${adapter.id} cannot generate video`,
9429
9492
  model: model.id
9430
9493
  });
9494
+ const startedAt = Date.now();
9431
9495
  const job = await adapter.generateVideo(request, ctx);
9432
9496
  const result = job.result.bind(job);
9433
9497
  let settled;
@@ -9437,11 +9501,14 @@ function createModelKit(config, adapters) {
9437
9501
  result(opts) {
9438
9502
  if (settled) return settled;
9439
9503
  const pending = result(opts).then(
9440
- (out) => emit({
9441
- ...out.usage,
9442
- model: model.id,
9443
- unit: model.usage.unit
9444
- }).then(() => out),
9504
+ async (out) => {
9505
+ const usage2 = timed(
9506
+ { ...out.usage, model: model.id, unit: model.usage.unit },
9507
+ startedAt
9508
+ );
9509
+ await emit(usage2);
9510
+ return { ...out, usage: usage2 };
9511
+ },
9445
9512
  (cause) => {
9446
9513
  settled = void 0;
9447
9514
  throw cause;
@@ -9478,9 +9545,14 @@ function createModelKit(config, adapters) {
9478
9545
  model: model.id
9479
9546
  });
9480
9547
  }
9548
+ const startedAt = Date.now();
9481
9549
  const result = await adapter.generateAudio(resolved, ctx);
9482
- await emit({ ...result.usage, model: model.id, unit: model.usage.unit });
9483
- return result;
9550
+ const usage2 = timed(
9551
+ { ...result.usage, model: model.id, unit: model.usage.unit },
9552
+ startedAt
9553
+ );
9554
+ await emit(usage2);
9555
+ return { ...result, usage: usage2 };
9484
9556
  },
9485
9557
  async prepareMediaPart(modelId, ref, mediaType, call = {}) {
9486
9558
  mediaRetryOptions(call);
@@ -9509,11 +9581,13 @@ function createModelKit(config, adapters) {
9509
9581
  const { webhookUrl, ...rest } = call;
9510
9582
  const { adapter, ctx } = videoToolTarget(req.model, rest);
9511
9583
  if (!adapter.submitVideoTool) throw noVideoTools(adapter, ctx.model);
9512
- return adapter.submitVideoTool(
9584
+ const submittedAt = Date.now();
9585
+ const submission = await adapter.submitVideoTool(
9513
9586
  req,
9514
9587
  ctx,
9515
9588
  webhookUrl !== void 0 ? { webhookUrl } : {}
9516
9589
  );
9590
+ return { ...submission, submittedAt };
9517
9591
  },
9518
9592
  async videoToolStatus(sub, call = {}) {
9519
9593
  const { adapter, ctx } = videoToolTarget(sub.model, call);
@@ -9525,8 +9599,10 @@ function createModelKit(config, adapters) {
9525
9599
  const { adapter, ctx } = videoToolTarget(sub.model, rest);
9526
9600
  if (!adapter.videoToolResult) throw noVideoTools(adapter, ctx.model);
9527
9601
  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;
9602
+ const base = { ...result.usage, model: ctx.model.id, unit: ctx.model.usage.unit };
9603
+ const usage2 = sub.submittedAt === void 0 ? base : timed(base, sub.submittedAt);
9604
+ await emit(usage2);
9605
+ return { ...result, usage: usage2 };
9530
9606
  },
9531
9607
  async cancelVideoTool(sub, call = {}) {
9532
9608
  const { adapter, ctx } = videoToolTarget(sub.model, call);
@@ -9548,6 +9624,7 @@ function createModelKit(config, adapters) {
9548
9624
  (model) => textTarget(model, call),
9549
9625
  options.providerOptions
9550
9626
  );
9627
+ const startedAt = Date.now();
9551
9628
  const result = await aiGenerateText2({
9552
9629
  ...options,
9553
9630
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9557,7 +9634,8 @@ function createModelKit(config, adapters) {
9557
9634
  options.prepareStep ?? options.experimental_prepareStep
9558
9635
  )
9559
9636
  });
9560
- for (const event of steps.usage(result.steps)) await emit(withRowUnit(event));
9637
+ for (const event of steps.usage(result.steps))
9638
+ await emit(textTimed(withRowUnit(event), result.steps.length, startedAt));
9561
9639
  return result;
9562
9640
  },
9563
9641
  streamText(options, call = {}) {
@@ -9568,6 +9646,7 @@ function createModelKit(config, adapters) {
9568
9646
  options.providerOptions
9569
9647
  );
9570
9648
  const callerOnFinish = options.onFinish;
9649
+ const startedAt = Date.now();
9571
9650
  return aiStreamText2({
9572
9651
  ...options,
9573
9652
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9577,7 +9656,8 @@ function createModelKit(config, adapters) {
9577
9656
  options.prepareStep ?? options.experimental_prepareStep
9578
9657
  ),
9579
9658
  onFinish: async (event) => {
9580
- for (const usage2 of steps.usage(event.steps)) await emit(withRowUnit(usage2));
9659
+ for (const usage2 of steps.usage(event.steps))
9660
+ await emit(textTimed(withRowUnit(usage2), event.steps.length, startedAt));
9581
9661
  await callerOnFinish?.(event);
9582
9662
  }
9583
9663
  });
@@ -9590,15 +9670,22 @@ function createModelKit(config, adapters) {
9590
9670
  model: languageModel2,
9591
9671
  providerOptions: mergeTextProviderOptions(entry, options.providerOptions)
9592
9672
  };
9673
+ const startedAt = Date.now();
9593
9674
  const result = await aiGenerateObject2(sdkOptions);
9594
9675
  await emit(
9595
- withRowUnit(textUsageEvent(entry, provider, result.usage, result.providerMetadata))
9676
+ timed(
9677
+ withRowUnit(
9678
+ textUsageEvent(entry, provider, result.usage, result.providerMetadata)
9679
+ ),
9680
+ startedAt
9681
+ )
9596
9682
  );
9597
9683
  return result;
9598
9684
  },
9599
9685
  streamObject(options, call = {}) {
9600
9686
  const { entry, languageModel: languageModel2, provider } = textTarget(options.model, call);
9601
9687
  const callerOnFinish = options.onFinish;
9688
+ const startedAt = Date.now();
9602
9689
  const sdkOptions = {
9603
9690
  ...options,
9604
9691
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9606,8 +9693,16 @@ function createModelKit(config, adapters) {
9606
9693
  providerOptions: mergeTextProviderOptions(entry, options.providerOptions),
9607
9694
  onFinish: async (event) => {
9608
9695
  await emit(
9609
- withRowUnit(
9610
- textUsageEvent(entry, provider, event.usage, event.providerMetadata)
9696
+ timed(
9697
+ withRowUnit(
9698
+ textUsageEvent(
9699
+ entry,
9700
+ provider,
9701
+ event.usage,
9702
+ event.providerMetadata
9703
+ )
9704
+ ),
9705
+ startedAt
9611
9706
  )
9612
9707
  );
9613
9708
  await callerOnFinish?.(event);