@shop-os/modelkit 0.1.0 → 0.1.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/README.md CHANGED
@@ -101,6 +101,11 @@ fetch, logging, and the `onUsage` sink.
101
101
  pass-through instances; catalog rows carry their own and the caller's are dropped
102
102
  with an `option_ignored` warning.
103
103
 
104
+ Text catalog rows carry no reasoning defaults. A catalog id behaves like the
105
+ provider's own default, so `temperature` and friends work as they do in `ai`.
106
+ Opt into thinking per call, for example
107
+ `providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 4000 } } }`.
108
+
104
109
  The kit keeps its `Warning` type for coercions; the SDK's warning type is `AiWarning`.
105
110
  `languageModel()` remains the low-level handle for Mastra and custom middleware.
106
111
 
package/dist/index.cjs CHANGED
@@ -212,6 +212,44 @@ var IMAGE_MODELS = [
212
212
  usage: { ...IMAGE_USAGE },
213
213
  pricingKind: "image_size"
214
214
  },
215
+ {
216
+ // Same Images API surface as gpt-image-2 plus the "xhigh" and "max" quality tiers.
217
+ id: "gpt-image-2.5-sunburst",
218
+ aliases: [],
219
+ label: "GPT Image 2.5 Sunburst",
220
+ type: "image",
221
+ adapter: "openai-image",
222
+ providerModelId: "gpt-image-2.5-sunburst",
223
+ lifecycle: "active",
224
+ constraints: {
225
+ aspectRatios: ["1:1", "3:2", "2:3", "16:9", "9:16", "4:5", "5:4", "adaptive"],
226
+ sizes: ["1K", "2K", "4K"],
227
+ references: { images: 16 },
228
+ supports: ["quality", "count"]
229
+ },
230
+ defaults: { size: "1K", aspectRatio: "adaptive", quality: "auto" },
231
+ usage: { ...IMAGE_USAGE },
232
+ pricingKind: "image_size"
233
+ },
234
+ {
235
+ // Same Images API surface as gpt-image-2 plus the "xhigh" and "max" quality tiers.
236
+ id: "gpt-image-2.5-flare",
237
+ aliases: [],
238
+ label: "GPT Image 2.5 Flare",
239
+ type: "image",
240
+ adapter: "openai-image",
241
+ providerModelId: "gpt-image-2.5-flare",
242
+ lifecycle: "active",
243
+ constraints: {
244
+ aspectRatios: ["1:1", "3:2", "2:3", "16:9", "9:16", "4:5", "5:4", "adaptive"],
245
+ sizes: ["1K", "2K", "4K"],
246
+ references: { images: 16 },
247
+ supports: ["quality", "count"]
248
+ },
249
+ defaults: { size: "1K", aspectRatio: "adaptive", quality: "auto" },
250
+ usage: { ...IMAGE_USAGE },
251
+ pricingKind: "image_size"
252
+ },
215
253
  {
216
254
  // Cowork allows 7 ratios; trigger-workflows allows 9 (adds 3:2, 9:21, drops 4:5/5:4).
217
255
  id: "muse-image-1.0",
@@ -588,77 +626,42 @@ function text(id, label, providerModelId, extra) {
588
626
  ...extra
589
627
  };
590
628
  }
591
- var ANTHROPIC_THINKING = {
592
- anthropic: {
593
- thinking: { type: "enabled", budgetTokens: 4e3 },
594
- sendReasoning: true
595
- }
596
- };
597
- var ANTHROPIC_ADAPTIVE = {
598
- anthropic: {
599
- thinking: { type: "adaptive", display: "summarized" },
600
- sendReasoning: true
601
- }
602
- };
603
- var OPENAI_REASONING = {
604
- openai: {
605
- reasoningEffort: "medium",
606
- reasoningSummary: "auto"
607
- }
608
- };
609
- var DEEPSEEK_THINKING = {
610
- deepseek: {
611
- thinking: { type: "enabled" }
612
- }
613
- };
614
629
  var TEXT_MODELS = [
615
630
  text("gpt-4o", "GPT-4o", "gpt-4o"),
616
631
  text("gpt-4o-mini", "GPT-4o mini", "gpt-4o-mini"),
617
632
  text("gpt-5.1", "GPT-5.1", "gpt-5.1"),
618
- text("gpt-5.1-mini", "GPT-5.1 mini", "gpt-5.1-mini"),
619
633
  text("gpt-5.2", "GPT-5.2", "gpt-5.2"),
620
- text("gpt-5.2-mini", "GPT-5.2 mini", "gpt-5.2-mini"),
621
634
  text("gpt-5.4", "GPT-5.4", "gpt-5.4"),
622
- text("gpt-5.4-mini", "GPT-5.4 mini", "gpt-5.4-mini"),
623
- text("gpt-5.5", "GPT-5.5", "gpt-5.5"),
624
- text("gpt-5.6-luna", "GPT-5.6 Luna", "gpt-5.6-luna", {
625
- providerOptions: { ...OPENAI_REASONING }
626
- }),
627
- text("gpt-5.6-terra", "GPT-5.6 Terra", "gpt-5.6-terra", {
628
- providerOptions: { ...OPENAI_REASONING }
629
- }),
630
- text("gpt-5.6-sol", "GPT-5.6 Sol", "gpt-5.6-sol", {
631
- providerOptions: { ...OPENAI_REASONING }
632
- }),
633
- // Dated 4.0 ids are what vibe-api, pro-admin and the TW geo agents still send.
634
- text("claude-sonnet-4", "Claude Sonnet 4", "claude-sonnet-4-20250514", {
635
- aliases: ["claude-sonnet-4.0", "claude-sonnet-4-20250514"]
636
- }),
637
- text("claude-opus-4", "Claude Opus 4", "claude-opus-4-20250514", {
638
- aliases: ["claude-opus-4.0", "claude-opus-4-20250514"]
635
+ // OpenAI never shipped gpt-5.1-mini or gpt-5.2-mini; callers that still send them get 5.4 mini.
636
+ text("gpt-5.4-mini", "GPT-5.4 mini", "gpt-5.4-mini", {
637
+ aliases: ["gpt-5.1-mini", "gpt-5.2-mini"]
639
638
  }),
639
+ text("gpt-5.5", "GPT-5.5", "gpt-5.5"),
640
+ text("gpt-5.6-luna", "GPT-5.6 Luna", "gpt-5.6-luna"),
641
+ text("gpt-5.6-terra", "GPT-5.6 Terra", "gpt-5.6-terra"),
642
+ text("gpt-5.6-sol", "GPT-5.6 Sol", "gpt-5.6-sol"),
640
643
  // Library key "claude-sonnet-4.5" maps to the SDK id "claude-sonnet-4-5".
641
644
  text("claude-sonnet-4-5", "Claude Sonnet 4.5", "claude-sonnet-4-5", {
642
645
  aliases: ["claude-sonnet-4.5"]
643
646
  }),
647
+ // Anthropic retired the dated 4.0 ids; callers that still send them get 4.6.
644
648
  text("claude-sonnet-4-6", "Claude Sonnet 4.6", "claude-sonnet-4-6", {
645
- aliases: ["claude-sonnet-4.6"],
646
- providerOptions: { ...ANTHROPIC_THINKING }
647
- }),
648
- text("claude-sonnet-5", "Claude Sonnet 5", "claude-sonnet-5", {
649
- providerOptions: { ...ANTHROPIC_ADAPTIVE }
649
+ aliases: [
650
+ "claude-sonnet-4.6",
651
+ "claude-sonnet-4",
652
+ "claude-sonnet-4.0",
653
+ "claude-sonnet-4-20250514"
654
+ ]
650
655
  }),
656
+ text("claude-sonnet-5", "Claude Sonnet 5", "claude-sonnet-5"),
651
657
  text("claude-opus-4-8", "Claude Opus 4.8", "claude-opus-4-8", {
652
- aliases: ["claude-opus-4.8"],
653
- providerOptions: { ...ANTHROPIC_ADAPTIVE }
658
+ aliases: ["claude-opus-4.8", "claude-opus-4", "claude-opus-4.0", "claude-opus-4-20250514"]
654
659
  }),
655
660
  text("claude-haiku-4-5", "Claude Haiku 4.5", "claude-haiku-4-5", {
656
- aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001"],
657
- providerOptions: { ...ANTHROPIC_THINKING }
661
+ aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001"]
658
662
  }),
659
663
  text("claude-opus-5", "Claude Opus 5", "claude-opus-5", {
660
- aliases: ["claude-opus-5.0"],
661
- providerOptions: { ...ANTHROPIC_ADAPTIVE }
664
+ aliases: ["claude-opus-5.0"]
662
665
  }),
663
666
  text("claude-fable-5", "Claude Fable 5", "claude-fable-5"),
664
667
  text("gemini-2.5-flash", "Gemini 2.5 Flash", "gemini-2.5-flash"),
@@ -677,12 +680,8 @@ var TEXT_MODELS = [
677
680
  text("sonar", "Sonar", "sonar"),
678
681
  text("sonar-pro", "Sonar Pro", "sonar-pro"),
679
682
  text("sonar-reasoning-pro", "Sonar Reasoning Pro", "sonar-reasoning-pro"),
680
- text("deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek-v4-flash", {
681
- providerOptions: { ...DEEPSEEK_THINKING }
682
- }),
683
- text("deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek-v4-pro", {
684
- providerOptions: { ...DEEPSEEK_THINKING }
685
- }),
683
+ text("deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek-v4-flash"),
684
+ text("deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek-v4-pro"),
686
685
  text("grok-4.3", "Grok 4.3", "grok-4.3"),
687
686
  text("kimi-k2.5", "Kimi K2.5", "kimi-k2.5"),
688
687
  text("kimi-k3", "Kimi K3", "kimi-k3"),
@@ -1513,23 +1512,46 @@ async function withRetry(fn, opts = {}) {
1513
1512
  }
1514
1513
  function sleep(ms, signal) {
1515
1514
  return new Promise((resolve, reject) => {
1516
- const t = setTimeout(resolve, ms);
1517
- signal?.addEventListener(
1518
- "abort",
1519
- () => {
1520
- clearTimeout(t);
1521
- reject(
1522
- new ModelKitError({ code: "timeout", message: "aborted", retryable: false })
1523
- );
1524
- },
1525
- { once: true }
1526
- );
1515
+ const rejectAborted = () => {
1516
+ reject(new ModelKitError({ code: "timeout", message: "aborted", retryable: false }));
1517
+ };
1518
+ if (signal?.aborted) {
1519
+ rejectAborted();
1520
+ return;
1521
+ }
1522
+ let timer;
1523
+ const onAbort = () => {
1524
+ if (timer !== void 0) clearTimeout(timer);
1525
+ rejectAborted();
1526
+ };
1527
+ timer = setTimeout(() => {
1528
+ signal?.removeEventListener("abort", onAbort);
1529
+ resolve();
1530
+ }, ms);
1531
+ signal?.addEventListener("abort", onAbort, { once: true });
1527
1532
  });
1528
1533
  }
1534
+ function throwAborted() {
1535
+ return new ModelKitError({ code: "timeout", message: "aborted", retryable: false });
1536
+ }
1537
+ function transportError(provider, cause, model) {
1538
+ const message = cause instanceof Error ? cause.message : String(cause);
1539
+ const aborted = cause instanceof Error && cause.name === "AbortError";
1540
+ const init = {
1541
+ code: aborted ? "timeout" : "network",
1542
+ message: `${provider} request failed before a response: ${message}`,
1543
+ provider,
1544
+ retryable: !aborted,
1545
+ cause
1546
+ };
1547
+ if (model !== void 0) init.model = model;
1548
+ return new ModelKitError(init);
1549
+ }
1529
1550
  async function pollUntil(check, opts) {
1530
1551
  const start = Date.now();
1531
1552
  let interval = opts.intervalMs;
1532
1553
  for (; ; ) {
1554
+ if (opts.signal?.aborted) throw throwAborted();
1533
1555
  const value = await check();
1534
1556
  if (value !== void 0) return value;
1535
1557
  if (Date.now() - start > opts.timeoutMs)
@@ -1538,6 +1560,7 @@ async function pollUntil(check, opts) {
1538
1560
  message: `poll timed out after ${opts.timeoutMs}ms`,
1539
1561
  retryable: false
1540
1562
  });
1563
+ if (opts.signal?.aborted) throw throwAborted();
1541
1564
  await sleep(interval, opts.signal);
1542
1565
  if (opts.backoff)
1543
1566
  interval = Math.min(opts.maxIntervalMs ?? interval * 4, interval * opts.backoff);
@@ -1715,7 +1738,7 @@ var OPENAI_COMPATIBLE_BASE_URLS = {
1715
1738
  var GATEWAY_SLUGS = {
1716
1739
  openai: "openai",
1717
1740
  anthropic: "anthropic",
1718
- google: "google-ai-studio",
1741
+ google: "google-ai-studio/v1beta",
1719
1742
  xai: "grok",
1720
1743
  perplexity: "perplexity-ai",
1721
1744
  deepseek: "deepseek"
@@ -2141,13 +2164,21 @@ function createModelKit(config, adapters) {
2141
2164
  });
2142
2165
  const job = await adapter.generateVideo(request, ctx);
2143
2166
  const result = job.result.bind(job);
2167
+ let settled;
2144
2168
  return {
2145
2169
  ...job,
2146
2170
  warnings,
2147
- async result(opts) {
2148
- const out = await result(opts);
2149
- await emit({ ...out.usage, model: model.id });
2150
- return out;
2171
+ result(opts) {
2172
+ if (settled) return settled;
2173
+ const pending = result(opts).then(
2174
+ (out) => emit({ ...out.usage, model: model.id }).then(() => out),
2175
+ (cause) => {
2176
+ settled = void 0;
2177
+ throw cause;
2178
+ }
2179
+ );
2180
+ settled = pending;
2181
+ return pending;
2151
2182
  }
2152
2183
  };
2153
2184
  },
@@ -2961,7 +2992,7 @@ async function generateFalVideo(req, ctx, deps) {
2961
2992
  if (isKling(providerModelId)) {
2962
2993
  const input = klingO1Input(req);
2963
2994
  if (ctx.routing.fal === "cloudflare-passthrough") {
2964
- return passthroughVideoJob(ctx, req, KLING_O1_ENDPOINT, input);
2995
+ return await passthroughVideoJob(ctx, req, KLING_O1_ENDPOINT, input);
2965
2996
  }
2966
2997
  const result2 = await runJob(ctx, deps, KLING_O1_ENDPOINT, input);
2967
2998
  return completedVideoJob(
@@ -2996,7 +3027,7 @@ async function generateFalVideo(req, ctx, deps) {
2996
3027
  base.aspect_ratio = req.aspectRatio ?? "adaptive";
2997
3028
  }
2998
3029
  if (ctx.routing.fal === "cloudflare-passthrough") {
2999
- return passthroughVideoJob(ctx, req, endpoint, base);
3030
+ return await passthroughVideoJob(ctx, req, endpoint, base);
3000
3031
  }
3001
3032
  const result = await runJob(ctx, deps, endpoint, base);
3002
3033
  return completedVideoJob(ctx, req, extractVideoUrl(result, endpoint, ctx.model.id), result);
@@ -3014,31 +3045,26 @@ function completedVideoJob(ctx, req, url, raw) {
3014
3045
  }
3015
3046
  };
3016
3047
  }
3017
- function passthroughVideoJob(ctx, req, endpoint, input) {
3048
+ async function passthroughVideoJob(ctx, req, endpoint, input) {
3018
3049
  const cfg = gatewayConfig(ctx);
3019
- let statusUrl;
3020
- const submit = (async () => {
3021
- const res = await gatewayFetch(cfg, ctx.fetch, `${FAL_QUEUE_HOST}/${endpoint}`, {
3022
- method: "POST",
3023
- body: JSON.stringify({ ...input, sync_mode: true })
3050
+ const res = await gatewayFetch(cfg, ctx.fetch, `${FAL_QUEUE_HOST}/${endpoint}`, {
3051
+ method: "POST",
3052
+ body: JSON.stringify({ ...input, sync_mode: true })
3053
+ });
3054
+ if (!res.ok) throw falError(res.status, await readBody(res), ctx.model.id);
3055
+ const parsed = falQueueSubmitSchema.safeParse(await res.json());
3056
+ if (!parsed.success) {
3057
+ throw new ModelKitError({
3058
+ code: "provider_rejected",
3059
+ message: `fal ${endpoint} submit returned no status URL`,
3060
+ provider: "fal",
3061
+ model: ctx.model.id,
3062
+ retryable: false
3024
3063
  });
3025
- if (!res.ok) throw falError(res.status, await readBody(res), ctx.model.id);
3026
- const parsed = falQueueSubmitSchema.safeParse(await res.json());
3027
- if (!parsed.success) {
3028
- throw new ModelKitError({
3029
- code: "provider_rejected",
3030
- message: `fal ${endpoint} submit returned no status URL`,
3031
- provider: "fal",
3032
- model: ctx.model.id,
3033
- retryable: false
3034
- });
3035
- }
3036
- statusUrl = parsed.data.status_url;
3037
- return parsed.data;
3038
- })();
3064
+ }
3065
+ const { request_id: jobId, status_url, response_url } = parsed.data;
3039
3066
  const checkStatus = async () => {
3040
- const { response_url } = await submit;
3041
- const statusRes = await gatewayFetch(cfg, ctx.fetch, statusUrl ?? "", { method: "GET" });
3067
+ const statusRes = await gatewayFetch(cfg, ctx.fetch, status_url, { method: "GET" });
3042
3068
  if (!statusRes.ok)
3043
3069
  throw falError(statusRes.status, await readBody(statusRes), ctx.model.id);
3044
3070
  const statusParsed = falQueueStatusSchema.safeParse(await statusRes.json());
@@ -3052,7 +3078,7 @@ function passthroughVideoJob(ctx, req, endpoint, input) {
3052
3078
  };
3053
3079
  const duration = req.durationSeconds ?? 5;
3054
3080
  return {
3055
- jobId: endpoint,
3081
+ jobId,
3056
3082
  async poll() {
3057
3083
  try {
3058
3084
  const url = await checkStatus();
@@ -4618,6 +4644,7 @@ var openaiImageAdapter = {
4618
4644
  const refs = req.references ?? [];
4619
4645
  const isEdit = refs.length > 0;
4620
4646
  const quality = req.quality ?? (isEdit ? "medium" : "auto");
4647
+ const sdkQuality = quality;
4621
4648
  const count = req.count !== void 0 && req.count > 1 ? { n: req.count } : {};
4622
4649
  const retryOptions = {};
4623
4650
  if (ctx.signal !== void 0) retryOptions.signal = ctx.signal;
@@ -4629,14 +4656,14 @@ var openaiImageAdapter = {
4629
4656
  model: ctx.model.providerModelId,
4630
4657
  prompt: req.prompt,
4631
4658
  size,
4632
- quality,
4659
+ quality: sdkQuality,
4633
4660
  ...count
4634
4661
  });
4635
4662
  }
4636
4663
  return await client2.images.generate({
4637
4664
  model: ctx.model.providerModelId,
4638
4665
  prompt: req.prompt,
4639
- quality,
4666
+ quality: sdkQuality,
4640
4667
  ...count
4641
4668
  });
4642
4669
  }
@@ -4655,7 +4682,7 @@ var openaiImageAdapter = {
4655
4682
  image: imageInput,
4656
4683
  prompt: req.prompt,
4657
4684
  size,
4658
- quality,
4685
+ quality: sdkQuality,
4659
4686
  ...count
4660
4687
  });
4661
4688
  }
@@ -4663,7 +4690,7 @@ var openaiImageAdapter = {
4663
4690
  model: ctx.model.providerModelId,
4664
4691
  image: imageInput,
4665
4692
  prompt: req.prompt,
4666
- quality,
4693
+ quality: sdkQuality,
4667
4694
  ...count
4668
4695
  });
4669
4696
  } catch (cause) {
@@ -4884,7 +4911,7 @@ async function generateProdiaImage(req, ctx) {
4884
4911
  });
4885
4912
  } catch (err) {
4886
4913
  if (isModelKitError(err)) throw err;
4887
- throw prodiaError(void 0, String(err), ctx.model.id);
4914
+ throw transportError("prodia", err, ctx.model.id);
4888
4915
  }
4889
4916
  if (!attemptRes.ok) {
4890
4917
  const text2 = await attemptRes.text().catch(() => "");
@@ -4894,7 +4921,7 @@ async function generateProdiaImage(req, ctx) {
4894
4921
  }, retryOpts);
4895
4922
  } catch (err) {
4896
4923
  if (isModelKitError(err)) throw err;
4897
- throw prodiaError(void 0, String(err), ctx.model.id);
4924
+ throw transportError("prodia", err, ctx.model.id);
4898
4925
  }
4899
4926
  const bytes = parseImageResponse(
4900
4927
  new Uint8Array(await res.arrayBuffer()),
@@ -5182,6 +5209,27 @@ function klingInput(req) {
5182
5209
  if (req.negativePrompt !== void 0) input.negative_prompt = req.negativePrompt;
5183
5210
  return input;
5184
5211
  }
5212
+ function klingOmniInput(req) {
5213
+ const images = (req.firstFrame ? [req.firstFrame] : []).map(refToUrl3);
5214
+ if (req.lastFrame) images.push(refToUrl3(req.lastFrame));
5215
+ const input = {
5216
+ prompt: req.prompt,
5217
+ duration: req.durationSeconds ?? 10,
5218
+ mode: req.mode ?? "standard",
5219
+ generate_audio: req.generateAudio ?? false
5220
+ };
5221
+ const startImage = images[0];
5222
+ if (startImage !== void 0) {
5223
+ input.start_image = startImage;
5224
+ const endImage = images[1];
5225
+ if (endImage !== void 0) input.end_image = endImage;
5226
+ } else {
5227
+ input.aspect_ratio = req.aspectRatio ?? "16:9";
5228
+ }
5229
+ const refs = (req.referenceImages ?? []).map(refToUrl3);
5230
+ if (refs.length > 0) input.reference_images = refs;
5231
+ return input;
5232
+ }
5185
5233
  function hailuoInput(req) {
5186
5234
  const input = {
5187
5235
  prompt: req.prompt,
@@ -5204,7 +5252,7 @@ function wanInput(req) {
5204
5252
  }
5205
5253
  async function generateReplicateVideo(req, ctx, deps) {
5206
5254
  const id = isLower(ctx.model.providerModelId);
5207
- const input = id.includes("kling") ? klingInput(req) : id.includes("hailuo") ? hailuoInput(req) : wanInput(req);
5255
+ const input = id.includes("kling") ? id.includes("omni") ? klingOmniInput(req) : klingInput(req) : id.includes("hailuo") ? hailuoInput(req) : wanInput(req);
5208
5256
  const api = client(ctx, deps);
5209
5257
  const createOptions = { ...route(ctx.model.providerModelId), input };
5210
5258
  let prediction;
@@ -5294,6 +5342,9 @@ function lyriaAudioInput(req, ctx) {
5294
5342
  if (negative) input.negative_prompt = negative;
5295
5343
  return input;
5296
5344
  }
5345
+ function elevenLabsMusicDuration(seconds) {
5346
+ return Math.min(300, Math.max(5, seconds ?? 15));
5347
+ }
5297
5348
  function elevenLabsMusicInput(req, ctx) {
5298
5349
  const prompt = req.prompt.trim();
5299
5350
  if (!prompt) {
@@ -5305,10 +5356,9 @@ function elevenLabsMusicInput(req, ctx) {
5305
5356
  retryable: false
5306
5357
  });
5307
5358
  }
5308
- const seconds = Math.min(300, Math.max(5, req.durationSeconds ?? 15));
5309
5359
  return {
5310
5360
  prompt,
5311
- music_length_ms: seconds * 1e3,
5361
+ music_length_ms: elevenLabsMusicDuration(req.durationSeconds) * 1e3,
5312
5362
  force_instrumental: req.instrumental ?? true,
5313
5363
  output_format: "wav_cd_quality"
5314
5364
  };
@@ -5348,7 +5398,7 @@ async function generateReplicateAudio(req, ctx, deps) {
5348
5398
  const input = id.includes("mmaudio") ? mmaudioInput(req, ctx) : id.includes("elevenlabs") ? elevenLabsMusicInput(req, ctx) : lyriaAudioInput(req, ctx);
5349
5399
  const { url, predictTime, raw } = await runPrediction(ctx, input, deps);
5350
5400
  const usage2 = audioUsage(ctx, req);
5351
- usage2.quantity = ctx.model.usage.unit === "seconds" ? req.durationSeconds ?? 0 : predictTime;
5401
+ usage2.quantity = ctx.model.usage.unit === "seconds" ? elevenLabsMusicDuration(req.durationSeconds) : predictTime;
5352
5402
  return { audio: { url }, usage: usage2, raw: predictionRaw(raw) };
5353
5403
  }
5354
5404