@shop-os/modelkit 0.1.3 → 0.1.4

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
@@ -89,13 +89,13 @@ cannot express (image `quality`/`outputFormat`, video `sourceVideo`, tiered
89
89
  `resolution`, ...) is dropped with an `option_ignored` warning, never silently. Use it
90
90
  for a model the catalog does not carry yet; the next step is a catalog row.
91
91
 
92
- | `ai` function | Here |
93
- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
94
- | `generateText`, `streamText` | Same. Catalog `providerOptions` are merged under the caller's; `prepareStep` model switches resolve through the catalog; usage per step. |
95
- | `generateObject`, `streamObject` | Same, including `output: "array" \| "enum" \| "no-schema"`. |
96
- | `generateImage` | Takes `n`, `size: "WxH"`, `aspectRatio`, `seed`, `abortSignal`, `providerOptions` plus the kit's `references`, `size: "2K"`, `quality`, `outputFormat`. Returns `image` and `images` with `uint8Array`, `base64`, `mediaType` next to `bytes`, `mime`. |
97
- | `experimental_generateVideo` | The kit's `generateVideo` returns a job; `duration` is an alias of `durationSeconds`. An `ai` video model instance is wrapped as a settled job. The `ai` function is also re-exported as-is. |
98
- | `experimental_generateSpeech`, `transcribe`, `embed`, `embedMany`, `rerank`, agents, UI streams | Re-exported from `ai` unchanged; the catalog has no rows for them. |
92
+ | `ai` function | Here |
93
+ | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `generateText`, `streamText` | Same. Catalog `providerOptions` are merged under the caller's; `prepareStep` model switches resolve through the catalog; usage per step. |
95
+ | `generateObject`, `streamObject` | Same, including `output: "array" \| "enum" \| "no-schema"`. |
96
+ | `generateImage` | Takes `n`, `size: "WxH"`, `aspectRatio`, `seed`, `abortSignal`, `providerOptions` plus the kit's `references`, `size: "2K"`, `quality`, `outputFormat`, `scaleFactor` (upscalers). Returns `image` and `images` with `uint8Array`, `base64`, `mediaType` next to `bytes`, `mime`; `outputUrl` when the provider hands back a URL. |
97
+ | `experimental_generateVideo` | The kit's `generateVideo` returns a job; `duration` is an alias of `durationSeconds`. An `ai` video model instance is wrapped as a settled job. The `ai` function is also re-exported as-is. |
98
+ | `experimental_generateSpeech`, `transcribe`, `embed`, `embedMany`, `rerank`, agents, UI streams | Re-exported from `ai` unchanged; the catalog has no rows for them. |
99
99
 
100
100
  An optional second `CallContext` argument selects enterprise keys, adds cancellation
101
101
  (combined with `abortSignal`), and sets `onUnsupported`. Package-level functions read
@@ -117,16 +117,16 @@ The kit keeps its `Warning` type for coercions; the SDK's warning type is `AiWar
117
117
 
118
118
  ## What the package owns
119
119
 
120
- | Concern | Rule |
121
- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
- | Ids | `id` is the pro-backend pricing key. Every historical id (`nb-pro`, `veo-3.1-generate-001`, `kling-v2.5-turbo-pro`) is an alias. |
123
- | Options | `resolveOptions` fills catalog defaults, coerces aspect ratio to the nearest supported one, errors on unsupported size or resolution (`onUnsupported: "coerce"` snaps down instead), clamps duration, trims references (audio first, then video), and drops options the model ignores with a warning. |
124
- | Keys | Enterprise keys win only when every key the adapter (and its route, e.g. the cloudflare token for gateway-routed text) needs is set on the enterprise tier. |
125
- | Routing | Cloudflare AI Gateway for text only. Never for Gemini images or Omni. fal passthrough for geo-blocked regions. OpenRouter optional for text. |
126
- | Retry | One classifier. Never on content blocks or 4xx except 429. |
127
- | Errors | `ModelKitError` with `code`, `provider`, `status`, `body`. Gemini `blockReason` and fal `body.detail` are lifted into `body`. |
128
- | Usage | One event per call with the canonical model id. Video usage is emitted on completion. Audio: Lyria 3 returns no duration, so it emits `unit: "images", quantity: 1` (token counts ride along as `inputTokens`/`outputTokens`); Replicate audio (Lyria 2, MMAudio) emits `unit: "compute_seconds"` with the prediction time. |
129
- | Media parts | `prepareMediaPart` turns a `MediaRef` into an AI SDK file part. Google inlines bytes up to 20 MB and uploads larger payloads through the Files API; call the returned `cleanup` after the request. Other providers get the reference inline. |
120
+ | Concern | Rule |
121
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
+ | Ids | `id` is the pro-backend pricing key. Every historical id (`nb-pro`, `veo-3.1-generate-001`, `kling-v2.5-turbo-pro`) is an alias. |
123
+ | Options | Image tiers are `512` (Gemini 3.1 Flash only), `1K`, `2K`, `4K`. `resolveOptions` fills catalog defaults, coerces aspect ratio to the nearest supported one, errors on unsupported size or resolution (`onUnsupported: "coerce"` snaps down instead), clamps duration, trims references (audio first, then video), and drops options the model ignores with a warning. |
124
+ | Keys | Enterprise keys win only when every key the adapter (and its route, e.g. the cloudflare token for gateway-routed text) needs is set on the enterprise tier. |
125
+ | Routing | Cloudflare AI Gateway for text only. Never for Gemini images or Omni. fal passthrough for geo-blocked regions. OpenRouter optional for text. |
126
+ | Retry | One classifier. Never on content blocks or 4xx except 429. |
127
+ | Errors | `ModelKitError` with `code`, `provider`, `status`, `body`. Gemini `blockReason` and fal `body.detail` are lifted into `body`. |
128
+ | Usage | One event per call with the canonical model id. Gemini image events carry the token counts too (`inputTokens`/`outputTokens`); Google's raw `usageMetadata` rides on `raw.providerMetadata.google`. Video usage is emitted on completion. Audio: Lyria 3 returns no duration, so it emits `unit: "images", quantity: 1` (token counts ride along as `inputTokens`/`outputTokens`); Replicate audio (Lyria 2, MMAudio) emits `unit: "compute_seconds"` with the prediction time. |
129
+ | Media parts | `prepareMediaPart` turns a `MediaRef` into an AI SDK file part. Google inlines bytes up to 20 MB and uploads larger payloads through the Files API; call the returned `cleanup` after the request. Other providers get the reference inline. |
130
130
 
131
131
  ## Add a model
132
132
 
package/dist/index.cjs CHANGED
@@ -165,7 +165,18 @@ var Catalog = class _Catalog {
165
165
 
166
166
  // src/catalog/image.ts
167
167
  var IMAGE_USAGE = { unit: "images", modality: "IMAGE" };
168
- var GEMINI_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "4:5", "5:4"];
168
+ var GEMINI_RATIOS = [
169
+ "1:1",
170
+ "2:3",
171
+ "3:2",
172
+ "16:9",
173
+ "9:16",
174
+ "4:3",
175
+ "3:4",
176
+ "21:9",
177
+ "4:5",
178
+ "5:4"
179
+ ];
169
180
  function geminiImage(id, label, aliases, sizes) {
170
181
  const model = {
171
182
  id,
@@ -188,7 +199,8 @@ var IMAGE_MODELS = [
188
199
  "gemini-3.1-flash-image-preview",
189
200
  "Nano Banana Flash",
190
201
  ["nb-2"],
191
- ["1K", "2K", "4K"]
202
+ // Flash 3.1 is the only Gemini image model with the 512 tier.
203
+ ["512", "1K", "2K", "4K"]
192
204
  ),
193
205
  geminiImage("gemini-3.1-flash-lite-image", "Nano Banana Lite", ["nb-lite"], ["1K"]),
194
206
  // trigger-workflows validates 1K only; pricing seeds 2K/4K rows so allow all three.
@@ -533,8 +545,12 @@ var OTHER_MODELS = [
533
545
  adapter: "replicate",
534
546
  providerModelId: "topazlabs/image-upscale",
535
547
  lifecycle: "active",
536
- constraints: {},
537
- defaults: {},
548
+ constraints: {
549
+ references: { images: 1 },
550
+ requires: ["references"],
551
+ supports: ["scaleFactor", "outputFormat"]
552
+ },
553
+ defaults: { scaleFactor: 2, outputFormat: "png" },
538
554
  usage: { unit: "compute_seconds", modality: "IMAGE" },
539
555
  pricingKind: "flat"
540
556
  },
@@ -590,7 +606,7 @@ var TEXT_MODELS = [
590
606
  text("gpt-5.6-sol", "GPT-5.6 Sol", "gpt-5.6-sol"),
591
607
  // Library key "claude-sonnet-4.5" maps to the SDK id "claude-sonnet-4-5".
592
608
  text("claude-sonnet-4-5", "Claude Sonnet 4.5", "claude-sonnet-4-5", {
593
- aliases: ["claude-sonnet-4.5"]
609
+ aliases: ["claude-sonnet-4.5", "claude-sonnet-4-5-20250929"]
594
610
  }),
595
611
  // Anthropic retired the dated 4.0 ids; callers that still send them get 4.6.
596
612
  text("claude-sonnet-4-6", "Claude Sonnet 4.6", "claude-sonnet-4-6", {
@@ -606,7 +622,7 @@ var TEXT_MODELS = [
606
622
  aliases: ["claude-opus-4.8", "claude-opus-4", "claude-opus-4.0", "claude-opus-4-20250514"]
607
623
  }),
608
624
  text("claude-haiku-4-5", "Claude Haiku 4.5", "claude-haiku-4-5", {
609
- aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001"]
625
+ aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001", "claude-haiku-4"]
610
626
  }),
611
627
  text("claude-opus-5", "Claude Opus 5", "claude-opus-5", {
612
628
  aliases: ["claude-opus-5.0"]
@@ -1089,7 +1105,7 @@ var MODELS = [
1089
1105
  ];
1090
1106
 
1091
1107
  // src/resolve-options.ts
1092
- var SIZE_ORDER = ["1K", "2K", "4K"];
1108
+ var SIZE_ORDER = ["512", "1K", "2K", "4K"];
1093
1109
  var RESOLUTION_ORDER = ["360p", "480p", "720p", "768p", "1080p", "2k", "4k"];
1094
1110
  function ratioValue(r) {
1095
1111
  if (r === "adaptive") return 1;
@@ -1221,7 +1237,13 @@ function resolveImageOptions(model, request, onUnsupported = "error") {
1221
1237
  model: model.id
1222
1238
  });
1223
1239
  }
1224
- dropUnsupported(merged, model, warnings, ["seed", "quality", "count", "outputFormat"]);
1240
+ dropUnsupported(merged, model, warnings, [
1241
+ "seed",
1242
+ "quality",
1243
+ "count",
1244
+ "outputFormat",
1245
+ "scaleFactor"
1246
+ ]);
1225
1247
  return { request: merged, warnings };
1226
1248
  }
1227
1249
  function resolveVideoOptions(model, request, onUnsupported = "error") {
@@ -1995,6 +2017,7 @@ function createModelKit(config, adapters) {
1995
2017
  providerOptions,
1996
2018
  references,
1997
2019
  exactSize: exactSizeField,
2020
+ scaleFactor,
1998
2021
  quality: _quality,
1999
2022
  outputFormat: _outputFormat
2000
2023
  } = req;
@@ -2020,6 +2043,8 @@ function createModelKit(config, adapters) {
2020
2043
  if (_quality !== void 0) pushIgnored(warnings, "quality", _quality, model.modelId);
2021
2044
  if (_outputFormat !== void 0)
2022
2045
  pushIgnored(warnings, "outputFormat", _outputFormat, model.modelId);
2046
+ if (scaleFactor !== void 0)
2047
+ pushIgnored(warnings, "scaleFactor", scaleFactor, model.modelId);
2023
2048
  const abortSignal = combineSignals(req.abortSignal, call.signal);
2024
2049
  if (abortSignal) options.abortSignal = abortSignal;
2025
2050
  const result = await (0, import_ai2.generateImage)(options);
@@ -4077,6 +4102,10 @@ function createGoogleImageAdapter(deps = defaultGoogleImageDeps) {
4077
4102
  imageQuality: "auto"
4078
4103
  };
4079
4104
  if (req.size !== void 0) usage2.imageSize = req.size;
4105
+ if (result.usage?.inputTokens !== void 0)
4106
+ usage2.inputTokens = result.usage.inputTokens;
4107
+ if (result.usage?.outputTokens !== void 0)
4108
+ usage2.outputTokens = result.usage.outputTokens;
4080
4109
  return { images, usage: usage2, raw: result };
4081
4110
  }
4082
4111
  };
@@ -4608,7 +4637,7 @@ function aspectRatioToSize(ratio, tier) {
4608
4637
  };
4609
4638
  const MIN_PIXELS = 655360;
4610
4639
  const MAX_PIXELS = 8294400;
4611
- const maxLong = TIER_LONG_EDGE[tier];
4640
+ const maxLong = TIER_LONG_EDGE[tier] ?? 1024;
4612
4641
  const RATIO_MAP = /* @__PURE__ */ new Map([
4613
4642
  ["1:1", [1, 1]],
4614
4643
  ["16:9", [16, 9]],
@@ -5242,6 +5271,9 @@ function isUpscaler(providerModelId) {
5242
5271
  const id = isLower(providerModelId);
5243
5272
  return id.includes("upscal") || id.includes("crystal") || id.includes("topaz");
5244
5273
  }
5274
+ function isTopazUpscaler(providerModelId) {
5275
+ return isLower(providerModelId).includes("topazlabs/image-upscale");
5276
+ }
5245
5277
  function isQwen(providerModelId) {
5246
5278
  return isLower(providerModelId).includes("qwen");
5247
5279
  }
@@ -5291,8 +5323,36 @@ function wanImageSize(aspectRatio, size) {
5291
5323
  }
5292
5324
  async function generateReplicateImage(req, ctx, deps) {
5293
5325
  const id = ctx.model.providerModelId;
5294
- const input = { prompt: req.prompt };
5295
- if (isUpscaler(id)) {
5326
+ let input;
5327
+ if (isTopazUpscaler(id)) {
5328
+ const first = (req.references ?? [])[0];
5329
+ if (!first) {
5330
+ throw new ModelKitError({
5331
+ code: "missing_input",
5332
+ message: `${ctx.model.id} requires a reference image to upscale`,
5333
+ provider: "replicate",
5334
+ model: ctx.model.id,
5335
+ retryable: false
5336
+ });
5337
+ }
5338
+ let format = req.outputFormat === "jpeg" ? "jpg" : req.outputFormat ?? "png";
5339
+ if (format !== "png" && format !== "jpg") {
5340
+ ctx.warnings.push({
5341
+ code: "option_ignored",
5342
+ field: "outputFormat",
5343
+ from: format,
5344
+ message: "topazlabs/image-upscale supports only png and jpg; using png"
5345
+ });
5346
+ format = "png";
5347
+ }
5348
+ input = {
5349
+ image: refToUrl3(first),
5350
+ enhance_model: "Standard V2",
5351
+ output_format: format,
5352
+ upscale_factor: `${req.scaleFactor ?? 2}x`,
5353
+ face_enhancement: true
5354
+ };
5355
+ } else if (isUpscaler(id)) {
5296
5356
  const refs = req.references ?? [];
5297
5357
  const first = refs[0];
5298
5358
  if (!first) {
@@ -5304,24 +5364,29 @@ async function generateReplicateImage(req, ctx, deps) {
5304
5364
  retryable: false
5305
5365
  });
5306
5366
  }
5307
- input.image = refToUrl3(first);
5308
- input.creativity = 0;
5309
- input.scale_factor = 2;
5310
- input.output_format = "png";
5367
+ const imageInput = { prompt: req.prompt };
5368
+ imageInput.image = refToUrl3(first);
5369
+ imageInput.creativity = 0;
5370
+ imageInput.scale_factor = 2;
5371
+ imageInput.output_format = "png";
5372
+ input = imageInput;
5311
5373
  } else if (isWanImage(id)) {
5312
5374
  const refs = (req.references ?? []).map(refToUrl3);
5313
- if (refs.length > 0) input.images = refs;
5314
- input.size = wanImageSize(req.aspectRatio, req.size);
5375
+ const imageInput = { prompt: req.prompt };
5376
+ if (refs.length > 0) imageInput.images = refs;
5377
+ imageInput.size = wanImageSize(req.aspectRatio, req.size);
5378
+ input = imageInput;
5315
5379
  } else if (isQwen(id)) {
5316
5380
  const refs = (req.references ?? []).map(refToUrl3);
5317
5381
  const firstUrl = refs[0];
5382
+ const imageInput = { prompt: req.prompt };
5318
5383
  if (firstUrl !== void 0) {
5319
- input.image = firstUrl;
5320
- if (!isQwenEditPlus(id)) input.match_input_image = true;
5384
+ imageInput.image = firstUrl;
5385
+ if (!isQwenEditPlus(id)) imageInput.match_input_image = true;
5321
5386
  }
5322
5387
  const ratios = isQwenEditPlus(id) ? QWEN_EDIT_RATIOS : QWEN_IMAGE_RATIOS;
5323
5388
  if (req.aspectRatio !== void 0) {
5324
- if (ratios.has(req.aspectRatio)) input.aspect_ratio = req.aspectRatio;
5389
+ if (ratios.has(req.aspectRatio)) imageInput.aspect_ratio = req.aspectRatio;
5325
5390
  else
5326
5391
  ctx.warnings.push({
5327
5392
  code: "option_ignored",
@@ -5331,18 +5396,21 @@ async function generateReplicateImage(req, ctx, deps) {
5331
5396
  });
5332
5397
  }
5333
5398
  if (isQwenEditPlus(id) && req.outputFormat !== void 0) {
5334
- input.output_format = req.outputFormat === "jpeg" ? "jpg" : req.outputFormat;
5399
+ imageInput.output_format = req.outputFormat === "jpeg" ? "jpg" : req.outputFormat;
5335
5400
  }
5401
+ input = imageInput;
5336
5402
  } else {
5337
5403
  const refs = req.references ?? [];
5338
5404
  const first = refs[0];
5339
- if (first) input.image = refToUrl3(first);
5405
+ const imageInput = { prompt: req.prompt };
5406
+ if (first) imageInput.image = refToUrl3(first);
5407
+ input = imageInput;
5340
5408
  }
5341
5409
  const { url, predictTime, raw } = await runPrediction(ctx, input, deps);
5342
5410
  const file = await download(ctx, url);
5343
5411
  const usage2 = imageUsage2(ctx, req);
5344
5412
  usage2.quantity = predictTime;
5345
- return { images: [{ bytes: file.bytes, mime: file.mime }], usage: usage2, raw };
5413
+ return { images: [{ bytes: file.bytes, mime: file.mime }], usage: usage2, raw, outputUrl: url };
5346
5414
  }
5347
5415
  function wanSize(aspectRatio) {
5348
5416
  if (!aspectRatio || aspectRatio === "adaptive" || aspectRatio === "1:1") return "2K";