@shop-os/modelkit 0.3.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -9,6 +9,30 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.3.4] - Unreleased
13
+
14
+ ### Added
15
+ - Snapshot rows `flux-video-upscale-precise` and `flux-video-upscale-creative` (fal `blackforestlabs/flux-video-upscale`, `type: "video-tool"`, upscale 1.5x to 3x, 4K long-edge limit, 20-second source), listed on `space_builder`. They share one endpoint and differ only in fal's `creativity` mode, which fal prices differently, so pricing keys them as two models.
16
+ - `constraints.tool.prompt` on upscale rows and `VideoToolRequest.prompt`: a row that sets it sends a trimmed, non-empty prompt to the provider. Other rows ignore the prompt.
17
+ - `providerOptions.fal.input` on video-tool rows: fixed body fields sent on every call. The source, factor and prompt are set after them, so a row cannot replace those.
18
+
19
+ ### Changed
20
+ - `topaz-precision` and `topaz-starlight` are also listed on `space_builder`.
21
+ - An upscale row with no Topaz model name (`providerOptions.fal.model`) no longer gets `model` or `H264_output` in its body. Both Topaz upscale rows name a model, so their bodies are unchanged.
22
+
23
+ ### Live rows to add after publish
24
+ Add the two FLUX rows, and add `space_builder` to the live Topaz Precision and Starlight rows, only after every app that calls them runs 0.3.4. An older kit sends `H264_output`, drops `creativity`, and so runs the precise row in creative mode.
25
+
26
+ ## [0.3.3] - Unreleased
27
+
28
+ ### Added
29
+ - `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`.
30
+ - `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.
31
+ - `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"`.
32
+
33
+ ### Live rows to add after publish
34
+ 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.
35
+
12
36
  ## [0.3.2] - Unreleased
13
37
 
14
38
  ### 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
@@ -2066,8 +2066,16 @@ var VIDEO_MODELS = [
2066
2066
 
2067
2067
  // src/catalog/video-tools.ts
2068
2068
  var TOPAZ_LOGO = "https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.97.0/light/topazlabs.png";
2069
+ var FLUX_LOGO = "https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.97.0/light/flux.png";
2069
2070
  var TOPAZ_SOURCE_SECONDS = { max: 300 };
2070
- var TOPAZ_USAGE = { unit: "seconds" };
2071
+ var SECONDS_USAGE = { unit: "seconds" };
2072
+ var FLUX_SOURCE_SECONDS = { max: 20 };
2073
+ var FLUX_UPSCALE_TOOL = {
2074
+ kind: "upscale",
2075
+ upscaleFactor: { min: 1.5, max: 3 },
2076
+ maxOutputLongEdge: 3840,
2077
+ prompt: true
2078
+ };
2071
2079
  var VIDEO_TOOL_MODELS = [
2072
2080
  {
2073
2081
  id: "topaz-precision",
@@ -2077,7 +2085,7 @@ var VIDEO_TOOL_MODELS = [
2077
2085
  adapter: "fal",
2078
2086
  providerModelId: "topaz/upscale/video/precision",
2079
2087
  lifecycle: "active",
2080
- exposure: ["cowork"],
2088
+ exposure: ["cowork", "space_builder"],
2081
2089
  constraints: {
2082
2090
  supports: [],
2083
2091
  sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
@@ -2085,7 +2093,7 @@ var VIDEO_TOOL_MODELS = [
2085
2093
  },
2086
2094
  defaults: {},
2087
2095
  providerOptions: { fal: { model: "Proteus" } },
2088
- usage: TOPAZ_USAGE,
2096
+ usage: SECONDS_USAGE,
2089
2097
  pricingKind: "video_per_second",
2090
2098
  display: {
2091
2099
  provider: "topaz",
@@ -2102,7 +2110,7 @@ var VIDEO_TOOL_MODELS = [
2102
2110
  adapter: "fal",
2103
2111
  providerModelId: "topaz/upscale/video/generative",
2104
2112
  lifecycle: "active",
2105
- exposure: ["cowork"],
2113
+ exposure: ["cowork", "space_builder"],
2106
2114
  constraints: {
2107
2115
  supports: [],
2108
2116
  sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
@@ -2110,7 +2118,7 @@ var VIDEO_TOOL_MODELS = [
2110
2118
  },
2111
2119
  defaults: {},
2112
2120
  providerOptions: { fal: { model: "Starlight Precise 2.6" } },
2113
- usage: TOPAZ_USAGE,
2121
+ usage: SECONDS_USAGE,
2114
2122
  pricingKind: "video_per_second",
2115
2123
  display: {
2116
2124
  provider: "topaz",
@@ -2135,7 +2143,7 @@ var VIDEO_TOOL_MODELS = [
2135
2143
  },
2136
2144
  defaults: {},
2137
2145
  providerOptions: { fal: { model: "Apollo" } },
2138
- usage: TOPAZ_USAGE,
2146
+ usage: SECONDS_USAGE,
2139
2147
  pricingKind: "video_per_second",
2140
2148
  display: {
2141
2149
  provider: "topaz",
@@ -2158,13 +2166,63 @@ var VIDEO_TOOL_MODELS = [
2158
2166
  tool: { kind: "hdr", outputFormats: ["h265", "prores"] }
2159
2167
  },
2160
2168
  defaults: {},
2161
- usage: TOPAZ_USAGE,
2169
+ usage: SECONDS_USAGE,
2162
2170
  pricingKind: "video_per_second",
2163
2171
  display: {
2164
2172
  provider: "topaz",
2165
2173
  logo: TOPAZ_LOGO,
2166
2174
  description: "Topaz Hyperion 2.5 SDR to HDR: a 10-bit HDR master in H.265 (HDR10) or ProRes 422 HQ, at the source's size and frame rate."
2167
2175
  }
2176
+ },
2177
+ {
2178
+ id: "flux-video-upscale-precise",
2179
+ aliases: [],
2180
+ label: "FLUX Upscale Precise",
2181
+ type: "video-tool",
2182
+ adapter: "fal",
2183
+ providerModelId: "blackforestlabs/flux-video-upscale",
2184
+ lifecycle: "active",
2185
+ exposure: ["space_builder"],
2186
+ constraints: {
2187
+ supports: [],
2188
+ sourceVideoSeconds: FLUX_SOURCE_SECONDS,
2189
+ tool: FLUX_UPSCALE_TOOL
2190
+ },
2191
+ defaults: {},
2192
+ providerOptions: { fal: { input: { creativity: 0 } } },
2193
+ usage: SECONDS_USAGE,
2194
+ pricingKind: "video_per_second",
2195
+ display: {
2196
+ provider: "black-forest-labs",
2197
+ logo: FLUX_LOGO,
2198
+ description: "FLUX 3 super-resolution, source-faithful: 1.5x to 3x, up to 4K, for clips up to 20 seconds.",
2199
+ sortOrder: 30
2200
+ }
2201
+ },
2202
+ {
2203
+ id: "flux-video-upscale-creative",
2204
+ aliases: [],
2205
+ label: "FLUX Upscale Creative",
2206
+ type: "video-tool",
2207
+ adapter: "fal",
2208
+ providerModelId: "blackforestlabs/flux-video-upscale",
2209
+ lifecycle: "active",
2210
+ exposure: ["space_builder"],
2211
+ constraints: {
2212
+ supports: [],
2213
+ sourceVideoSeconds: FLUX_SOURCE_SECONDS,
2214
+ tool: FLUX_UPSCALE_TOOL
2215
+ },
2216
+ defaults: {},
2217
+ providerOptions: { fal: { input: { creativity: 1 } } },
2218
+ usage: SECONDS_USAGE,
2219
+ pricingKind: "video_per_second",
2220
+ display: {
2221
+ provider: "black-forest-labs",
2222
+ logo: FLUX_LOGO,
2223
+ description: "FLUX 3 super-resolution that adds fine detail, optionally guided by a prompt: 1.5x to 3x, up to 4K, for clips up to 20 seconds. Costs more than Precise.",
2224
+ sortOrder: 40
2225
+ }
2168
2226
  }
2169
2227
  ];
2170
2228
 
@@ -2303,7 +2361,8 @@ var VIDEO_INPUT_SPEC_KEYS = [
2303
2361
  "generateAudio",
2304
2362
  "cfgScale",
2305
2363
  "promptExpansion",
2306
- "referenceImages"
2364
+ "referenceImages",
2365
+ "sourceVideo"
2307
2366
  ];
2308
2367
  var IMAGE_INPUT_SPEC_KEYS = [
2309
2368
  "prompt",
@@ -2322,6 +2381,12 @@ var AUDIO_INPUT_SPEC_KEYS = [
2322
2381
  "sourceVideo",
2323
2382
  "instrumental"
2324
2383
  ];
2384
+ var ARRAY_INPUT_SPEC_KEYS = [
2385
+ "firstFrame",
2386
+ "referenceImages",
2387
+ "lastFrame",
2388
+ "references"
2389
+ ];
2325
2390
  var INPUT_SPEC_SUPPORTS_KEYS = [
2326
2391
  "lastFrame",
2327
2392
  "seed",
@@ -2335,6 +2400,7 @@ var INPUT_SPEC_SUPPORTS_KEYS = [
2335
2400
  var VIDEO_KEY_SET = new Set(VIDEO_INPUT_SPEC_KEYS);
2336
2401
  var IMAGE_KEY_SET = new Set(IMAGE_INPUT_SPEC_KEYS);
2337
2402
  var AUDIO_KEY_SET = new Set(AUDIO_INPUT_SPEC_KEYS);
2403
+ var ARRAY_KEY_SET = new Set(ARRAY_INPUT_SPEC_KEYS);
2338
2404
  var ALL_KEY_SET = /* @__PURE__ */ new Set([
2339
2405
  ...VIDEO_INPUT_SPEC_KEYS,
2340
2406
  ...IMAGE_INPUT_SPEC_KEYS,
@@ -2379,13 +2445,30 @@ var ValuesSchema = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.re
2379
2445
  }
2380
2446
  });
2381
2447
  var StaticSchema = import_zod2.z.record(import_zod2.z.string(), ProviderScalarSchema);
2448
+ 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) => {
2449
+ for (const [name, keys] of Object.entries(arrays)) {
2450
+ for (const key of keys) {
2451
+ if (!ARRAY_KEY_SET.has(key)) {
2452
+ ctx.addIssue({
2453
+ code: "custom",
2454
+ path: [name],
2455
+ message: `inputSpec.arrays "${name}" has unknown image key "${key}"`
2456
+ });
2457
+ }
2458
+ }
2459
+ }
2460
+ });
2382
2461
  var InputSpecSchema = import_zod2.z.strictObject({
2383
2462
  fields: FieldsSchema,
2384
2463
  values: ValuesSchema.optional(),
2385
2464
  static: StaticSchema.optional(),
2465
+ arrays: ArraysSchema.optional(),
2386
2466
  editEndpoint: import_zod2.z.string().min(1).optional()
2387
2467
  }).superRefine((spec, ctx) => {
2388
- const mappedNames = new Set(Object.values(spec.fields));
2468
+ const mappedNames = /* @__PURE__ */ new Set([
2469
+ ...Object.values(spec.fields),
2470
+ ...Object.keys(spec.arrays ?? {})
2471
+ ]);
2389
2472
  for (const key of Object.keys(spec.static ?? {})) {
2390
2473
  if (mappedNames.has(key)) {
2391
2474
  ctx.addIssue({
@@ -2474,14 +2557,54 @@ function validateInputSpec(raw, row) {
2474
2557
  });
2475
2558
  }
2476
2559
  }
2560
+ const arrayOwner = /* @__PURE__ */ new Map();
2561
+ for (const [name, keys] of Object.entries(spec.arrays ?? {})) {
2562
+ const fieldKey = seenNames.get(name);
2563
+ if (fieldKey !== void 0) {
2564
+ issues.push({
2565
+ path: ["arrays", name],
2566
+ message: `inputSpec.arrays "${name}" is also the provider field of inputSpec.fields "${fieldKey}"`
2567
+ });
2568
+ }
2569
+ for (const key of keys) {
2570
+ if (allowed !== void 0 && !allowed.has(key)) {
2571
+ issues.push({
2572
+ path: ["arrays", name],
2573
+ message: `inputSpec.arrays "${key}" is not supported for ${row.type} rows`
2574
+ });
2575
+ }
2576
+ if (key in spec.fields) {
2577
+ issues.push({
2578
+ path: ["arrays", name],
2579
+ message: `inputSpec.arrays "${key}" is already mapped in inputSpec.fields`
2580
+ });
2581
+ }
2582
+ const owner = arrayOwner.get(key);
2583
+ if (owner === void 0) arrayOwner.set(key, name);
2584
+ else {
2585
+ issues.push({
2586
+ path: ["arrays", name],
2587
+ message: owner === name ? `inputSpec.arrays "${name}" lists "${key}" twice` : `inputSpec.arrays "${owner}" and "${name}" both take "${key}"`
2588
+ });
2589
+ }
2590
+ }
2591
+ }
2477
2592
  const supports = row.constraints.supports ?? [];
2478
2593
  for (const key of INPUT_SPEC_SUPPORTS_KEYS) {
2479
- if (key in spec.fields && !supports.includes(key)) {
2594
+ if (supports.includes(key)) continue;
2595
+ if (key in spec.fields) {
2480
2596
  issues.push({
2481
2597
  path: ["fields", key],
2482
2598
  message: `inputSpec.fields "${key}" needs "${key}" in constraints.supports`
2483
2599
  });
2484
2600
  }
2601
+ const arrayName = arrayOwner.get(key);
2602
+ if (arrayName !== void 0) {
2603
+ issues.push({
2604
+ path: ["arrays", arrayName],
2605
+ message: `inputSpec.arrays "${key}" needs "${key}" in constraints.supports`
2606
+ });
2607
+ }
2485
2608
  }
2486
2609
  if (issues.length > 0) return { ok: false, issues };
2487
2610
  return { ok: true, spec };
@@ -2606,6 +2729,7 @@ var VideoToolConstraintsSchema = import_zod3.z.looseObject({
2606
2729
  kind: import_zod3.z.enum(["upscale", "interpolate", "hdr"]),
2607
2730
  upscaleFactor: RangeSchema.optional(),
2608
2731
  maxOutputLongEdge: import_zod3.z.number().positive().optional(),
2732
+ prompt: import_zod3.z.boolean().optional(),
2609
2733
  fps: RangeSchema.optional(),
2610
2734
  outputFormats: import_zod3.z.array(import_zod3.z.enum(["h265", "prores"])).min(1).optional()
2611
2735
  });
@@ -2703,6 +2827,7 @@ var CatalogCapabilitiesSchema = import_zod3.z.looseObject({
2703
2827
  acceptsSourceVideo: import_zod3.z.boolean().optional(),
2704
2828
  videoEdit: import_zod3.z.boolean().optional(),
2705
2829
  framesExcludeReferences: import_zod3.z.boolean().optional(),
2830
+ sourceVideoExcludesFrames: import_zod3.z.boolean().optional(),
2706
2831
  lastFrameRequiresFirstFrame: import_zod3.z.boolean().optional(),
2707
2832
  reasoning: import_zod3.z.boolean().optional(),
2708
2833
  vision: import_zod3.z.boolean().optional(),
@@ -3063,6 +3188,7 @@ var ToolSchema = import_zod6.z.object({
3063
3188
  kind: import_zod6.z.enum(["upscale", "interpolate", "hdr"]),
3064
3189
  upscaleFactor: RangeSchema2.optional(),
3065
3190
  maxOutputLongEdge: import_zod6.z.number().positive().optional(),
3191
+ prompt: import_zod6.z.boolean().optional(),
3066
3192
  fps: RangeSchema2.optional(),
3067
3193
  outputFormats: import_zod6.z.array(import_zod6.z.enum(["h265", "prores"])).min(1).optional()
3068
3194
  });
@@ -3070,11 +3196,12 @@ function videoToolConstraints(row) {
3070
3196
  if (row.type !== "video-tool") return void 0;
3071
3197
  const parsed = ToolSchema.safeParse(row.constraints.tool);
3072
3198
  if (!parsed.success) return void 0;
3073
- const { kind, upscaleFactor, maxOutputLongEdge, fps, outputFormats } = parsed.data;
3199
+ const { kind, upscaleFactor, maxOutputLongEdge, prompt, fps, outputFormats } = parsed.data;
3074
3200
  return {
3075
3201
  kind,
3076
3202
  ...upscaleFactor !== void 0 && { upscaleFactor },
3077
3203
  ...maxOutputLongEdge !== void 0 && { maxOutputLongEdge },
3204
+ ...prompt !== void 0 && { prompt },
3078
3205
  ...fps !== void 0 && { fps },
3079
3206
  ...outputFormats !== void 0 && { outputFormats }
3080
3207
  };
@@ -3400,6 +3527,26 @@ function resolveVideoOptions(model, request, onUnsupported = "error") {
3400
3527
  });
3401
3528
  delete merged.sourceVideo;
3402
3529
  }
3530
+ const imageInputs = ["firstFrame", "lastFrame", "referenceImages"].filter(
3531
+ (field) => field === "referenceImages" ? compact(merged.referenceImages).length > 0 : merged[field] !== void 0
3532
+ );
3533
+ if (merged.sourceVideo !== void 0 && imageInputs.length > 0 && model.capabilities?.sourceVideoExcludesFrames) {
3534
+ if (onUnsupported === "error") {
3535
+ throw new ModelKitError({
3536
+ code: "unsupported_option",
3537
+ message: `${model.id} takes a source video or images, not both`,
3538
+ model: model.id
3539
+ });
3540
+ }
3541
+ for (const field of imageInputs) {
3542
+ warnings.push({
3543
+ code: "option_ignored",
3544
+ field,
3545
+ message: `${model.id} takes a source video or images, not both; ${field} dropped`
3546
+ });
3547
+ delete merged[field];
3548
+ }
3549
+ }
3403
3550
  if (merged.lastFrame !== void 0 && merged.firstFrame === void 0 && model.capabilities?.lastFrameRequiresFirstFrame) {
3404
3551
  if (onUnsupported === "error") {
3405
3552
  throw new ModelKitError({