@shop-os/modelkit 0.3.0 → 0.3.2

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.cjs CHANGED
@@ -286,13 +286,21 @@ var GEMINI_RATIOS = [
286
286
  "5:4",
287
287
  "adaptive"
288
288
  ];
289
+ var NANO_BANANA_2_1_RATIOS = [
290
+ ...GEMINI_RATIOS.filter((r) => r !== "adaptive"),
291
+ "1:4",
292
+ "4:1",
293
+ "1:8",
294
+ "8:1",
295
+ "adaptive"
296
+ ];
289
297
  var GEMINI_PROMPT_MAX_CHARS = 5e4;
290
298
  var GPT_IMAGE_PROMPT = { referenceSyntax: "numbered", maxChars: 32e3 };
291
- function geminiImage(id, label, aliases, sizes, display, exposure, references, promptMaxChars) {
299
+ function geminiImage(id, label, aliases, sizes, display, exposure, references, promptMaxChars, aspectRatios = GEMINI_RATIOS) {
292
300
  const prompt = { referenceSyntax: "caption" };
293
301
  if (promptMaxChars !== void 0) prompt.maxChars = promptMaxChars;
294
302
  const constraints = {
295
- aspectRatios: [...GEMINI_RATIOS],
303
+ aspectRatios: [...aspectRatios],
296
304
  sizes,
297
305
  supports: ["safetySettings", "temperature"],
298
306
  prompt
@@ -316,6 +324,21 @@ function geminiImage(id, label, aliases, sizes, display, exposure, references, p
316
324
  return model;
317
325
  }
318
326
  var IMAGE_MODELS = [
327
+ geminiImage(
328
+ "gemini-nano-banana-2.1",
329
+ "Nano Banana 2.1",
330
+ ["nb-2.1"],
331
+ ["1K", "2K", "4K"],
332
+ {
333
+ provider: "google",
334
+ description: "Latest high-efficiency Nano Banana. Better realism, prompt adherence, text rendering and character consistency at Flash speed. Great for campaigns, infographics, and iterative edits.",
335
+ logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
336
+ },
337
+ ["cowork", "space_builder"],
338
+ { images: 14 },
339
+ GEMINI_PROMPT_MAX_CHARS,
340
+ NANO_BANANA_2_1_RATIOS
341
+ ),
319
342
  geminiImage(
320
343
  "gemini-3-pro-image-preview",
321
344
  "Nano Banana Pro",
@@ -2369,6 +2392,10 @@ var ALL_KEY_SET = /* @__PURE__ */ new Set([
2369
2392
  ...IMAGE_INPUT_SPEC_KEYS,
2370
2393
  ...AUDIO_INPUT_SPEC_KEYS
2371
2394
  ]);
2395
+ var TYPES_BY_ADAPTER = /* @__PURE__ */ new Map([
2396
+ ["replicate", ["video", "image", "audio"]],
2397
+ ["fal", ["image"]]
2398
+ ]);
2372
2399
  var ALLOWED_KEYS_BY_TYPE = /* @__PURE__ */ new Map([
2373
2400
  ["video", VIDEO_KEY_SET],
2374
2401
  ["image", IMAGE_KEY_SET],
@@ -2407,7 +2434,8 @@ var StaticSchema = import_zod2.z.record(import_zod2.z.string(), ProviderScalarSc
2407
2434
  var InputSpecSchema = import_zod2.z.strictObject({
2408
2435
  fields: FieldsSchema,
2409
2436
  values: ValuesSchema.optional(),
2410
- static: StaticSchema.optional()
2437
+ static: StaticSchema.optional(),
2438
+ editEndpoint: import_zod2.z.string().min(1).optional()
2411
2439
  }).superRefine((spec, ctx) => {
2412
2440
  const mappedNames = new Set(Object.values(spec.fields));
2413
2441
  for (const key of Object.keys(spec.static ?? {})) {
@@ -2435,18 +2463,21 @@ function validateInputSpec(raw, row) {
2435
2463
  };
2436
2464
  }
2437
2465
  const spec = parsed.data;
2438
- if (row.adapter !== "replicate") {
2466
+ const supportedTypes = TYPES_BY_ADAPTER.get(row.adapter);
2467
+ if (supportedTypes === void 0) {
2439
2468
  return {
2440
2469
  ok: false,
2441
- issues: [{ path: [], message: "inputSpec is only supported on replicate rows" }]
2470
+ issues: [
2471
+ { path: [], message: "inputSpec is only supported on replicate and fal rows" }
2472
+ ]
2442
2473
  };
2443
2474
  }
2444
2475
  const issues = [];
2445
2476
  const allowed = ALLOWED_KEYS_BY_TYPE.get(row.type);
2446
- if (allowed === void 0) {
2477
+ if (allowed === void 0 || !supportedTypes.includes(row.type)) {
2447
2478
  issues.push({
2448
2479
  path: [],
2449
- message: `inputSpec is not supported for "${row.type}" rows`
2480
+ message: row.adapter === "fal" ? `inputSpec on fal rows is only supported for image rows, not "${row.type}"` : `inputSpec is not supported for "${row.type}" rows`
2450
2481
  });
2451
2482
  } else {
2452
2483
  for (const key of Object.keys(spec.fields)) {
@@ -2466,6 +2497,19 @@ function validateInputSpec(raw, row) {
2466
2497
  }
2467
2498
  }
2468
2499
  }
2500
+ if (spec.editEndpoint !== void 0) {
2501
+ if (row.adapter !== "fal") {
2502
+ issues.push({
2503
+ path: ["editEndpoint"],
2504
+ message: "inputSpec.editEndpoint is only supported on fal rows"
2505
+ });
2506
+ } else if (!("references" in spec.fields)) {
2507
+ issues.push({
2508
+ path: ["editEndpoint"],
2509
+ message: 'inputSpec.editEndpoint needs a "references" entry in inputSpec.fields'
2510
+ });
2511
+ }
2512
+ }
2469
2513
  const seenNames = /* @__PURE__ */ new Map();
2470
2514
  for (const [key, name] of Object.entries(spec.fields)) {
2471
2515
  const first = seenNames.get(name);
@@ -2541,6 +2585,10 @@ var AspectRatioSchema = import_zod3.z.enum([
2541
2585
  "21:9",
2542
2586
  "9:21",
2543
2587
  "97:30",
2588
+ "1:4",
2589
+ "4:1",
2590
+ "1:8",
2591
+ "8:1",
2544
2592
  "adaptive"
2545
2593
  ]);
2546
2594
  var ImageSizeSchema = import_zod3.z.enum(["512", "1K", "2K", "4K"]);
@@ -4210,6 +4258,70 @@ async function cancelFalVideoTool(sub, ctx) {
4210
4258
  throw falError(res.status, await readBody(res), ctx.model.id);
4211
4259
  }
4212
4260
 
4261
+ // src/adapters/input-spec-mapper.ts
4262
+ function canonicalValue(req, key) {
4263
+ return req[key];
4264
+ }
4265
+ function isMediaRef(value) {
4266
+ return value instanceof Object && ("url" in value || "bytes" in value);
4267
+ }
4268
+ function toProviderValue(value, refToUrl2) {
4269
+ if (Array.isArray(value)) {
4270
+ return value.map(refToUrl2);
4271
+ }
4272
+ if (isMediaRef(value)) {
4273
+ return refToUrl2(value);
4274
+ }
4275
+ return value;
4276
+ }
4277
+ function applyInputSpec(spec, req, refToUrl2, opts) {
4278
+ const out = {};
4279
+ for (const [canonical, providerField] of Object.entries(spec.fields)) {
4280
+ if (!isInputSpecKey(canonical)) continue;
4281
+ const value = canonicalValue(req, canonical);
4282
+ if (value === void 0) continue;
4283
+ if (opts?.omit?.(canonical, value)) continue;
4284
+ const serialised = toProviderValue(value, refToUrl2);
4285
+ const mapped = spec.values?.[canonical]?.[String(value)];
4286
+ out[providerField] = mapped ?? serialised;
4287
+ }
4288
+ for (const [key, value] of Object.entries(spec.static ?? {})) {
4289
+ out[key] = value;
4290
+ }
4291
+ return out;
4292
+ }
4293
+ function resolveRowInputSpec(req, ctx) {
4294
+ const raw = ctx.model.providerOptions?.["modelkit"]?.["inputSpec"];
4295
+ if (raw === void 0) return { kind: "absent" };
4296
+ const result = validateInputSpec(raw, ctx.model);
4297
+ if (!result.ok) {
4298
+ ctx.logger.warn(
4299
+ `[modelkit] ${ctx.model.id} has an invalid providerOptions.modelkit.inputSpec; the call fails`
4300
+ );
4301
+ return { kind: "invalid", issues: result.issues };
4302
+ }
4303
+ return {
4304
+ kind: "ok",
4305
+ spec: result.spec,
4306
+ input: applyInputSpec(result.spec, req, refToUrl, {
4307
+ omit: (key, value) => key === "aspectRatio" && value === "adaptive"
4308
+ })
4309
+ };
4310
+ }
4311
+ function describeInputSpecIssue(issue) {
4312
+ if (!issue) return "invalid inputSpec";
4313
+ return issue.path.length === 0 ? issue.message : `${issue.path.map((part) => String(part)).join(".")} ${issue.message}`;
4314
+ }
4315
+ function invalidInputSpecError(provider, ctx, issues) {
4316
+ return new ModelKitError({
4317
+ code: "unsupported_option",
4318
+ message: `${provider} row "${ctx.model.id}" has an invalid providerOptions.modelkit.inputSpec: ${describeInputSpecIssue(issues[0])}`,
4319
+ provider,
4320
+ model: ctx.model.id,
4321
+ retryable: false
4322
+ });
4323
+ }
4324
+
4213
4325
  // src/adapters/fal.ts
4214
4326
  var MUSE_T2I_ENDPOINT = "meta/muse-image/text-to-image";
4215
4327
  var MUSE_EDIT_ENDPOINT = "meta/muse-image/edit";
@@ -4264,6 +4376,11 @@ function createFalAdapter(deps = defaultDeps) {
4264
4376
  id: "fal",
4265
4377
  generateImage: (req, ctx) => {
4266
4378
  throwIfAborted(ctx.signal);
4379
+ const spec = resolveRowInputSpec(req, ctx);
4380
+ if (spec.kind === "invalid") {
4381
+ return Promise.reject(invalidInputSpecError("fal", ctx, spec.issues));
4382
+ }
4383
+ if (spec.kind === "ok") return generateSpecImage(req, ctx, deps, spec);
4267
4384
  if (isIdeogram(ctx.model.providerModelId)) return generateIdeogramImage(req, ctx, deps);
4268
4385
  return generateMuseImage(req, ctx, deps);
4269
4386
  },
@@ -4443,6 +4560,36 @@ var museImageResultSchema = import_zod12.z.object({
4443
4560
  })
4444
4561
  ).optional()
4445
4562
  });
4563
+ async function collectFalImages(payload, ctx, req, label) {
4564
+ const parsed = museImageResultSchema.safeParse(payload);
4565
+ const images = parsed.success ? parsed.data.images ?? [] : [];
4566
+ const files = await Promise.all(
4567
+ images.flatMap((img) => {
4568
+ if (img.url === void 0) return [];
4569
+ return [decodeImage(ctx, img.url, img.content_type ?? void 0)];
4570
+ })
4571
+ );
4572
+ if (files.length === 0) {
4573
+ throw new ModelKitError({
4574
+ code: "provider_rejected",
4575
+ message: `${label} returned no image`,
4576
+ provider: "fal",
4577
+ model: ctx.model.id,
4578
+ retryable: false
4579
+ });
4580
+ }
4581
+ return {
4582
+ images: files.map((f) => ({ bytes: f.bytes, mime: f.mime })),
4583
+ usage: imageUsage(ctx, req, files.length),
4584
+ raw: payload
4585
+ };
4586
+ }
4587
+ async function generateSpecImage(req, ctx, deps, spec) {
4588
+ const hasReferences = (req.references ?? []).length > 0;
4589
+ const endpoint = hasReferences && spec.spec.editEndpoint !== void 0 ? spec.spec.editEndpoint : ctx.model.providerModelId;
4590
+ const payload = await runJob(ctx, deps, endpoint, spec.input);
4591
+ return collectFalImages(payload, ctx, req, `fal ${endpoint}`);
4592
+ }
4446
4593
  async function generateMuseImage(req, ctx, deps) {
4447
4594
  const refs = (req.references ?? []).map(refToUrl);
4448
4595
  const isEdit = refs.length > 0;
@@ -4459,29 +4606,7 @@ async function generateMuseImage(req, ctx, deps) {
4459
4606
  const custom = id.includes("/") && !id.startsWith("meta/muse-image") ? id : void 0;
4460
4607
  const endpoint = custom ?? (isEdit ? MUSE_EDIT_ENDPOINT : MUSE_T2I_ENDPOINT);
4461
4608
  const payload = await runJob(ctx, deps, endpoint, input);
4462
- const parsed = museImageResultSchema.safeParse(payload);
4463
- const images = parsed.success ? parsed.data.images ?? [] : [];
4464
- const firstUrl = images[0]?.url;
4465
- if (images.length === 0 || firstUrl === void 0) {
4466
- throw new ModelKitError({
4467
- code: "provider_rejected",
4468
- message: "fal Meta Muse returned no image",
4469
- provider: "fal",
4470
- model: ctx.model.id,
4471
- retryable: false
4472
- });
4473
- }
4474
- const files = await Promise.all(
4475
- images.flatMap((img) => {
4476
- if (img.url === void 0) return [];
4477
- return [decodeImage(ctx, img.url, img.content_type ?? void 0)];
4478
- })
4479
- );
4480
- return {
4481
- images: files.map((f) => ({ bytes: f.bytes, mime: f.mime })),
4482
- usage: imageUsage(ctx, req, files.length),
4483
- raw: payload
4484
- };
4609
+ return collectFalImages(payload, ctx, req, "fal Meta Muse");
4485
4610
  }
4486
4611
  var IDEOGRAM_MODEL_IDS = /* @__PURE__ */ new Set(["ideogram/v4.5"]);
4487
4612
  function isIdeogram(providerModelId) {
@@ -4526,28 +4651,7 @@ async function generateIdeogramImage(req, ctx, deps) {
4526
4651
  if (references.length > 0) input.reference_image_urls = references;
4527
4652
  }
4528
4653
  const payload = await runJob(ctx, deps, endpoint, input);
4529
- const parsed = museImageResultSchema.safeParse(payload);
4530
- const images = parsed.success ? parsed.data.images ?? [] : [];
4531
- const files = await Promise.all(
4532
- images.flatMap((img) => {
4533
- if (img.url === void 0) return [];
4534
- return [decodeImage(ctx, img.url, img.content_type ?? void 0)];
4535
- })
4536
- );
4537
- if (files.length === 0) {
4538
- throw new ModelKitError({
4539
- code: "provider_rejected",
4540
- message: "fal Ideogram returned no image",
4541
- provider: "fal",
4542
- model: ctx.model.id,
4543
- retryable: false
4544
- });
4545
- }
4546
- return {
4547
- images: files.map((f) => ({ bytes: f.bytes, mime: f.mime })),
4548
- usage: imageUsage(ctx, req, files.length),
4549
- raw: payload
4550
- };
4654
+ return collectFalImages(payload, ctx, req, "fal Ideogram");
4551
4655
  }
4552
4656
  var H3_MAX_MODEL_IDS = /* @__PURE__ */ new Set([
4553
4657
  "minimax/h3-max",
@@ -6896,41 +7000,6 @@ function createReplicateHttpClient(opts) {
6896
7000
 
6897
7001
  // src/adapters/replicate.ts
6898
7002
  var import_zod19 = require("zod");
6899
-
6900
- // src/adapters/input-spec-mapper.ts
6901
- function canonicalValue(req, key) {
6902
- return req[key];
6903
- }
6904
- function isMediaRef(value) {
6905
- return value instanceof Object && ("url" in value || "bytes" in value);
6906
- }
6907
- function toProviderValue(value, refToUrl2) {
6908
- if (Array.isArray(value)) {
6909
- return value.map(refToUrl2);
6910
- }
6911
- if (isMediaRef(value)) {
6912
- return refToUrl2(value);
6913
- }
6914
- return value;
6915
- }
6916
- function applyInputSpec(spec, req, refToUrl2, opts) {
6917
- const out = {};
6918
- for (const [canonical, providerField] of Object.entries(spec.fields)) {
6919
- if (!isInputSpecKey(canonical)) continue;
6920
- const value = canonicalValue(req, canonical);
6921
- if (value === void 0) continue;
6922
- if (opts?.omit?.(canonical, value)) continue;
6923
- const serialised = toProviderValue(value, refToUrl2);
6924
- const mapped = spec.values?.[canonical]?.[String(value)];
6925
- out[providerField] = mapped ?? serialised;
6926
- }
6927
- for (const [key, value] of Object.entries(spec.static ?? {})) {
6928
- out[key] = value;
6929
- }
6930
- return out;
6931
- }
6932
-
6933
- // src/adapters/replicate.ts
6934
7003
  var POLL_INTERVAL_MS = 1e3;
6935
7004
  var POLL_TIMEOUT_MS = 15 * 6e4;
6936
7005
  var defaultReplicateDeps = {
@@ -7173,38 +7242,6 @@ function replicateNoMapper(ctx, section) {
7173
7242
  retryable: false
7174
7243
  });
7175
7244
  }
7176
- function describeInputSpecIssue(issue) {
7177
- if (!issue) return "invalid inputSpec";
7178
- return issue.path.length === 0 ? issue.message : `${issue.path.map((part) => String(part)).join(".")} ${issue.message}`;
7179
- }
7180
- function replicateInvalidSpec(ctx, issues) {
7181
- const detail = describeInputSpecIssue(issues[0]);
7182
- return new ModelKitError({
7183
- code: "unsupported_option",
7184
- message: `replicate row "${ctx.model.id}" has an invalid providerOptions.modelkit.inputSpec: ${detail}`,
7185
- provider: "replicate",
7186
- model: ctx.model.id,
7187
- retryable: false
7188
- });
7189
- }
7190
- function omitAdaptive(key, value) {
7191
- return key === "aspectRatio" && value === "adaptive";
7192
- }
7193
- function replicateInputSpec(req, ctx) {
7194
- const raw = ctx.model.providerOptions?.["modelkit"]?.["inputSpec"];
7195
- if (raw === void 0) return { kind: "absent" };
7196
- const result = validateInputSpec(raw, ctx.model);
7197
- if (!result.ok) {
7198
- ctx.logger.warn(
7199
- `[modelkit] ${ctx.model.id} has an invalid providerOptions.modelkit.inputSpec; the call fails`
7200
- );
7201
- return { kind: "invalid", issues: result.issues };
7202
- }
7203
- return {
7204
- kind: "ok",
7205
- input: applyInputSpec(result.spec, req, refToUrl, { omit: omitAdaptive })
7206
- };
7207
- }
7208
7245
  var QWEN_IMAGE_RATIOS = /* @__PURE__ */ new Set([
7209
7246
  "1:1",
7210
7247
  "16:9",
@@ -7348,8 +7385,9 @@ async function generateReplicateImage(req, ctx, deps) {
7348
7385
  break;
7349
7386
  }
7350
7387
  default: {
7351
- const specResult = replicateInputSpec(req, ctx);
7352
- if (specResult.kind === "invalid") throw replicateInvalidSpec(ctx, specResult.issues);
7388
+ const specResult = resolveRowInputSpec(req, ctx);
7389
+ if (specResult.kind === "invalid")
7390
+ throw invalidInputSpecError("replicate", ctx, specResult.issues);
7353
7391
  if (specResult.kind === "ok") {
7354
7392
  input = specResult.input;
7355
7393
  break;
@@ -7429,9 +7467,10 @@ async function generateReplicateVideo(req, ctx, deps) {
7429
7467
  else if (kind === "kling-omni") input = klingOmniInput(req);
7430
7468
  else if (kind === "hailuo") input = hailuoInput(req);
7431
7469
  else {
7432
- const specResult = replicateInputSpec(req, ctx);
7470
+ const specResult = resolveRowInputSpec(req, ctx);
7433
7471
  if (specResult.kind === "absent") throw replicateNoMapper(ctx, "video");
7434
- if (specResult.kind === "invalid") throw replicateInvalidSpec(ctx, specResult.issues);
7472
+ if (specResult.kind === "invalid")
7473
+ throw invalidInputSpecError("replicate", ctx, specResult.issues);
7435
7474
  input = specResult.input;
7436
7475
  }
7437
7476
  const api = client(ctx, deps);
@@ -7586,9 +7625,10 @@ async function generateReplicateAudio(req, ctx, deps) {
7586
7625
  else if (kind === "elevenlabs") input = elevenLabsMusicInput(req, ctx);
7587
7626
  else if (kind === "lyria") input = lyriaAudioInput(req, ctx);
7588
7627
  else {
7589
- const specResult = replicateInputSpec(req, ctx);
7628
+ const specResult = resolveRowInputSpec(req, ctx);
7590
7629
  if (specResult.kind === "absent") throw replicateNoMapper(ctx, "audio");
7591
- if (specResult.kind === "invalid") throw replicateInvalidSpec(ctx, specResult.issues);
7630
+ if (specResult.kind === "invalid")
7631
+ throw invalidInputSpecError("replicate", ctx, specResult.issues);
7592
7632
  input = specResult.input;
7593
7633
  }
7594
7634
  const { url, predictTime, raw } = await runPrediction(ctx, input, deps);