@shop-os/modelkit 0.3.2 → 0.3.3

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/CHANGELOG.md CHANGED
@@ -9,6 +9,16 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.3.3] - Unreleased
13
+
14
+ ### Added
15
+ - `inputSpec.arrays` joins several image inputs into one provider array field, for a model that takes its start frame, end frame and storyboard frames in one list. It maps a provider field to an ordered list of canonical keys: `arrays: { images: ["firstFrame", "referenceImages", "lastFrame"] }` sends the first frame, then the reference images, then the last frame. The field is omitted when none of the inputs is set. Video rows take `firstFrame`, `referenceImages` and `lastFrame`; image rows take `references`. A key is mapped once: in `fields` or in one array. An array name must not repeat a `fields` provider name or a `static` key, and `lastFrame` in an array needs `lastFrame` in `constraints.supports`.
16
+ - `sourceVideo` is an `inputSpec.fields` key on video rows, so a Replicate video row can send a source clip (for example `sourceVideo: "start_video"`). The row also needs `capabilities.acceptsSourceVideo`, or the resolver drops the clip before the adapter runs.
17
+ - `capabilities.sourceVideoExcludesFrames`: a source video is never sent with a first frame, a last frame or reference images. `resolveVideoOptions` refuses the mix under `"error"` and drops the images with an `option_ignored` warning for each under `"coerce"`.
18
+
19
+ ### Live rows to add after publish
20
+ Add a Replicate video row that uses `inputSpec.arrays` or maps `sourceVideo` (for example `black-forest-labs/flux-3`) only after every app that will call it runs 0.3.3. An older kit reads such a spec as invalid: the row stays in the catalog and every call to it fails. A row of this shape should set `capabilities.lastFrameRequiresFirstFrame`, because a lone end frame would be sent as the first element of the array.
21
+
12
22
  ## [0.3.2] - Unreleased
13
23
 
14
24
  ### Added
package/README.md CHANGED
@@ -218,12 +218,13 @@ instead of a kit change. It is read by the Replicate and fal adapters only. `fie
218
218
  `values` remaps canonical values per key, and `static` merges literal provider fields
219
219
  last. Video rows use `VIDEO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`,
220
220
  `aspectRatio`, `resolution`, `firstFrame`, `lastFrame`, `seed`, `negativePrompt`,
221
- `generateAudio`, `cfgScale`, `promptExpansion`, `referenceImages`); image rows use
221
+ `generateAudio`, `cfgScale`, `promptExpansion`, `referenceImages`, `sourceVideo`); image rows use
222
222
  `IMAGE_INPUT_SPEC_KEYS` (`prompt`, `references`, `aspectRatio`, `size`, `outputFormat`,
223
223
  `seed`; map `references` to the provider's array field and remap `size` and `outputFormat`
224
224
  with `values`); audio rows use
225
225
  `AUDIO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`, `seed`, `negativePrompt`,
226
226
  `references`, `sourceVideo`, `instrumental`). `aspectRatio: "adaptive"` is omitted. A fal image row has no kind and builds its whole input from the spec; the optional `editEndpoint` (fal rows only) is the fal endpoint called instead of `providerModelId` when the request has references. fal rows without an `inputSpec` keep the Muse-shaped request and Ideogram keeps its own path; an `inputSpec` on an Ideogram row replaces that path. `editEndpoint` needs a `references` entry in `fields`. `inputSpec` on fal video rows is not supported.
227
+ `arrays` joins several image inputs into one provider array field, in the listed order: `arrays: { images: ["firstFrame", "referenceImages", "lastFrame"] }` (video rows: `firstFrame`, `referenceImages`, `lastFrame`; image rows: `references`). A key is mapped once, in `fields` or in one array, and the field is omitted when none of its inputs is set. A row that maps `sourceVideo` also needs `capabilities.acceptsSourceVideo`; set `capabilities.sourceVideoExcludesFrames` when the provider rejects a source video next to images.
227
228
  `validateInputSpec` is the shared contract the write schema, `dataOnlyCapable`, and
228
229
  the Replicate mapper all check. `dataOnlyCapable(row)` reports whether such a row would
229
230
  be served.
package/dist/catalog.cjs CHANGED
@@ -2303,7 +2303,8 @@ var VIDEO_INPUT_SPEC_KEYS = [
2303
2303
  "generateAudio",
2304
2304
  "cfgScale",
2305
2305
  "promptExpansion",
2306
- "referenceImages"
2306
+ "referenceImages",
2307
+ "sourceVideo"
2307
2308
  ];
2308
2309
  var IMAGE_INPUT_SPEC_KEYS = [
2309
2310
  "prompt",
@@ -2322,6 +2323,12 @@ var AUDIO_INPUT_SPEC_KEYS = [
2322
2323
  "sourceVideo",
2323
2324
  "instrumental"
2324
2325
  ];
2326
+ var ARRAY_INPUT_SPEC_KEYS = [
2327
+ "firstFrame",
2328
+ "referenceImages",
2329
+ "lastFrame",
2330
+ "references"
2331
+ ];
2325
2332
  var INPUT_SPEC_SUPPORTS_KEYS = [
2326
2333
  "lastFrame",
2327
2334
  "seed",
@@ -2335,6 +2342,7 @@ var INPUT_SPEC_SUPPORTS_KEYS = [
2335
2342
  var VIDEO_KEY_SET = new Set(VIDEO_INPUT_SPEC_KEYS);
2336
2343
  var IMAGE_KEY_SET = new Set(IMAGE_INPUT_SPEC_KEYS);
2337
2344
  var AUDIO_KEY_SET = new Set(AUDIO_INPUT_SPEC_KEYS);
2345
+ var ARRAY_KEY_SET = new Set(ARRAY_INPUT_SPEC_KEYS);
2338
2346
  var ALL_KEY_SET = /* @__PURE__ */ new Set([
2339
2347
  ...VIDEO_INPUT_SPEC_KEYS,
2340
2348
  ...IMAGE_INPUT_SPEC_KEYS,
@@ -2379,13 +2387,30 @@ var ValuesSchema = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.re
2379
2387
  }
2380
2388
  });
2381
2389
  var StaticSchema = import_zod2.z.record(import_zod2.z.string(), ProviderScalarSchema);
2390
+ var ArraysSchema = import_zod2.z.record(import_zod2.z.string().min(1), import_zod2.z.array(import_zod2.z.string()).min(1)).superRefine((arrays, ctx) => {
2391
+ for (const [name, keys] of Object.entries(arrays)) {
2392
+ for (const key of keys) {
2393
+ if (!ARRAY_KEY_SET.has(key)) {
2394
+ ctx.addIssue({
2395
+ code: "custom",
2396
+ path: [name],
2397
+ message: `inputSpec.arrays "${name}" has unknown image key "${key}"`
2398
+ });
2399
+ }
2400
+ }
2401
+ }
2402
+ });
2382
2403
  var InputSpecSchema = import_zod2.z.strictObject({
2383
2404
  fields: FieldsSchema,
2384
2405
  values: ValuesSchema.optional(),
2385
2406
  static: StaticSchema.optional(),
2407
+ arrays: ArraysSchema.optional(),
2386
2408
  editEndpoint: import_zod2.z.string().min(1).optional()
2387
2409
  }).superRefine((spec, ctx) => {
2388
- const mappedNames = new Set(Object.values(spec.fields));
2410
+ const mappedNames = /* @__PURE__ */ new Set([
2411
+ ...Object.values(spec.fields),
2412
+ ...Object.keys(spec.arrays ?? {})
2413
+ ]);
2389
2414
  for (const key of Object.keys(spec.static ?? {})) {
2390
2415
  if (mappedNames.has(key)) {
2391
2416
  ctx.addIssue({
@@ -2474,14 +2499,54 @@ function validateInputSpec(raw, row) {
2474
2499
  });
2475
2500
  }
2476
2501
  }
2502
+ const arrayOwner = /* @__PURE__ */ new Map();
2503
+ for (const [name, keys] of Object.entries(spec.arrays ?? {})) {
2504
+ const fieldKey = seenNames.get(name);
2505
+ if (fieldKey !== void 0) {
2506
+ issues.push({
2507
+ path: ["arrays", name],
2508
+ message: `inputSpec.arrays "${name}" is also the provider field of inputSpec.fields "${fieldKey}"`
2509
+ });
2510
+ }
2511
+ for (const key of keys) {
2512
+ if (allowed !== void 0 && !allowed.has(key)) {
2513
+ issues.push({
2514
+ path: ["arrays", name],
2515
+ message: `inputSpec.arrays "${key}" is not supported for ${row.type} rows`
2516
+ });
2517
+ }
2518
+ if (key in spec.fields) {
2519
+ issues.push({
2520
+ path: ["arrays", name],
2521
+ message: `inputSpec.arrays "${key}" is already mapped in inputSpec.fields`
2522
+ });
2523
+ }
2524
+ const owner = arrayOwner.get(key);
2525
+ if (owner === void 0) arrayOwner.set(key, name);
2526
+ else {
2527
+ issues.push({
2528
+ path: ["arrays", name],
2529
+ message: owner === name ? `inputSpec.arrays "${name}" lists "${key}" twice` : `inputSpec.arrays "${owner}" and "${name}" both take "${key}"`
2530
+ });
2531
+ }
2532
+ }
2533
+ }
2477
2534
  const supports = row.constraints.supports ?? [];
2478
2535
  for (const key of INPUT_SPEC_SUPPORTS_KEYS) {
2479
- if (key in spec.fields && !supports.includes(key)) {
2536
+ if (supports.includes(key)) continue;
2537
+ if (key in spec.fields) {
2480
2538
  issues.push({
2481
2539
  path: ["fields", key],
2482
2540
  message: `inputSpec.fields "${key}" needs "${key}" in constraints.supports`
2483
2541
  });
2484
2542
  }
2543
+ const arrayName = arrayOwner.get(key);
2544
+ if (arrayName !== void 0) {
2545
+ issues.push({
2546
+ path: ["arrays", arrayName],
2547
+ message: `inputSpec.arrays "${key}" needs "${key}" in constraints.supports`
2548
+ });
2549
+ }
2485
2550
  }
2486
2551
  if (issues.length > 0) return { ok: false, issues };
2487
2552
  return { ok: true, spec };
@@ -2703,6 +2768,7 @@ var CatalogCapabilitiesSchema = import_zod3.z.looseObject({
2703
2768
  acceptsSourceVideo: import_zod3.z.boolean().optional(),
2704
2769
  videoEdit: import_zod3.z.boolean().optional(),
2705
2770
  framesExcludeReferences: import_zod3.z.boolean().optional(),
2771
+ sourceVideoExcludesFrames: import_zod3.z.boolean().optional(),
2706
2772
  lastFrameRequiresFirstFrame: import_zod3.z.boolean().optional(),
2707
2773
  reasoning: import_zod3.z.boolean().optional(),
2708
2774
  vision: import_zod3.z.boolean().optional(),
@@ -3400,6 +3466,26 @@ function resolveVideoOptions(model, request, onUnsupported = "error") {
3400
3466
  });
3401
3467
  delete merged.sourceVideo;
3402
3468
  }
3469
+ const imageInputs = ["firstFrame", "lastFrame", "referenceImages"].filter(
3470
+ (field) => field === "referenceImages" ? compact(merged.referenceImages).length > 0 : merged[field] !== void 0
3471
+ );
3472
+ if (merged.sourceVideo !== void 0 && imageInputs.length > 0 && model.capabilities?.sourceVideoExcludesFrames) {
3473
+ if (onUnsupported === "error") {
3474
+ throw new ModelKitError({
3475
+ code: "unsupported_option",
3476
+ message: `${model.id} takes a source video or images, not both`,
3477
+ model: model.id
3478
+ });
3479
+ }
3480
+ for (const field of imageInputs) {
3481
+ warnings.push({
3482
+ code: "option_ignored",
3483
+ field,
3484
+ message: `${model.id} takes a source video or images, not both; ${field} dropped`
3485
+ });
3486
+ delete merged[field];
3487
+ }
3488
+ }
3403
3489
  if (merged.lastFrame !== void 0 && merged.firstFrame === void 0 && model.capabilities?.lastFrameRequiresFirstFrame) {
3404
3490
  if (onUnsupported === "error") {
3405
3491
  throw new ModelKitError({