@howells/motif-sdk 5.3.0 → 6.0.0

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
@@ -2,10 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let neverthrow = require("neverthrow");
3
3
  let _howells_envy = require("@howells/envy");
4
4
  let zod = require("zod");
5
- let ai = require("ai");
6
- let _ai_sdk_fal = require("@ai-sdk/fal");
7
- let _ai_sdk_openai = require("@ai-sdk/openai");
8
- let _ai_sdk_replicate = require("@ai-sdk/replicate");
9
5
  //#region src/aspects.ts
10
6
  /** Ordered by popularity: square first, then common ratios */
11
7
  const ASPECT_RATIOS = [
@@ -355,7 +351,7 @@ const CREATIVE_TAXONOMY = {
355
351
  experimental: true,
356
352
  id: "illustration",
357
353
  label: "Editorial illustration",
358
- model: "gpt2"
354
+ model: "flare"
359
355
  }
360
356
  ],
361
357
  mood: [
@@ -538,7 +534,7 @@ function falHttpError(status, body, requestId) {
538
534
  * `CreativeOptionError`'s `"INVALID_OPTION"`) onto the returned error so callers
539
535
  * keep structured error metadata. Status `0` marks a non-HTTP local error.
540
536
  */
541
- function toMotifError$1(error) {
537
+ function toMotifError(error) {
542
538
  if (error instanceof MotifError) return error;
543
539
  return new MotifError(error instanceof Error ? error.message : String(error), 0, error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0);
544
540
  }
@@ -556,6 +552,7 @@ const CARRIED_FIELDS = [
556
552
  "video",
557
553
  "prompt",
558
554
  "mask",
555
+ "markup",
559
556
  "references",
560
557
  "boxes",
561
558
  "margin",
@@ -691,20 +688,13 @@ function promptWarnings(prompt, context = {}) {
691
688
  }
692
689
  //#endregion
693
690
  //#region src/env.ts
694
- const motifEnvSchema = (0, _howells_envy.defineEnv)({ optional: {
695
- FAL_KEY: zod.z.string().min(1),
696
- OPENAI_API_KEY: zod.z.string().min(1)
697
- } });
691
+ const motifEnvSchema = (0, _howells_envy.defineEnv)({ optional: { FAL_KEY: zod.z.string().min(1) } });
698
692
  function parseMotifEnv(input = process.env) {
699
693
  return motifEnvSchema.parse(input);
700
694
  }
701
695
  function getFalKeyFromEnv(input = process.env) {
702
696
  return parseMotifEnv(input).FAL_KEY;
703
697
  }
704
- /** The OpenAI key, used by direct OpenAI routes such as gpt2 transparency. */
705
- function getOpenAiKeyFromEnv(input = process.env) {
706
- return parseMotifEnv(input).OPENAI_API_KEY;
707
- }
708
698
  //#endregion
709
699
  //#region src/models.ts
710
700
  const AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-10-07";
@@ -729,7 +719,7 @@ const MODELS = {
729
719
  rank: 2
730
720
  }
731
721
  },
732
- useCase: "Frontier generation and editing, evaluated at max quality"
722
+ useCase: "Frontier generation and editing at max reasoning effort; scored as GPT Image 2.5 until 2.6 is benchmarked"
733
723
  },
734
724
  defaultQuality: "max",
735
725
  customImageSize: {
@@ -739,11 +729,11 @@ const MODELS = {
739
729
  minPixels: 655360,
740
730
  multipleOf: 16
741
731
  },
742
- editEndpoint: "openai/gpt-image-2.5/flare/edit",
743
- endpoint: "openai/gpt-image-2.5/flare/text-to-image",
732
+ editEndpoint: "openai/gpt-image-2.6/flare/edit",
733
+ endpoint: "openai/gpt-image-2.6/flare/text-to-image",
744
734
  maxReferenceImages: 16,
745
- name: "GPT Image 2.5 Flare",
746
- pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
735
+ name: "GPT Image 2.6 Flare",
736
+ pricing: "Token-based: image $8/M input, $2/M cached, $30/M output; text $5/M input, $1.25/M cached",
747
737
  pricePerImageUsd: null,
748
738
  sizeMode: "image_size_enum",
749
739
  supportsAspect: true,
@@ -751,8 +741,23 @@ const MODELS = {
751
741
  supportsEdit: true,
752
742
  supportsMaskImage: true,
753
743
  maskImageField: "mask_url",
744
+ supportsMarkupImage: true,
745
+ autoEditSize: true,
754
746
  supportsNumImages: true,
755
747
  supportsOutputFormat: true,
748
+ supportedOutputFormats: [
749
+ "jpeg",
750
+ "png",
751
+ "webp",
752
+ "gif",
753
+ "svg"
754
+ ],
755
+ supportedEditOutputFormats: [
756
+ "jpeg",
757
+ "png",
758
+ "webp",
759
+ "gif"
760
+ ],
756
761
  supportsQuality: true,
757
762
  supportedQualities: [
758
763
  "auto",
@@ -781,7 +786,7 @@ const MODELS = {
781
786
  rank: 1
782
787
  }
783
788
  },
784
- useCase: "Frontier generation and editing, evaluated at max quality"
789
+ useCase: "Frontier generation and editing at max reasoning effort; scored as GPT Image 2.5 until 2.6 is benchmarked"
785
790
  },
786
791
  defaultQuality: "max",
787
792
  customImageSize: {
@@ -791,11 +796,11 @@ const MODELS = {
791
796
  minPixels: 655360,
792
797
  multipleOf: 16
793
798
  },
794
- editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
795
- endpoint: "openai/gpt-image-2.5/sunburst/text-to-image",
799
+ editEndpoint: "openai/gpt-image-2.6/sunburst/edit",
800
+ endpoint: "openai/gpt-image-2.6/sunburst/text-to-image",
796
801
  maxReferenceImages: 16,
797
- name: "GPT Image 2.5 Sunburst",
798
- pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
802
+ name: "GPT Image 2.6 Sunburst",
803
+ pricing: "Token-based: image $8/M input, $2/M cached, $30/M output; text $5/M input, $1.25/M cached",
799
804
  pricePerImageUsd: null,
800
805
  sizeMode: "image_size_enum",
801
806
  supportsAspect: true,
@@ -803,8 +808,23 @@ const MODELS = {
803
808
  supportsEdit: true,
804
809
  supportsMaskImage: true,
805
810
  maskImageField: "mask_url",
811
+ supportsMarkupImage: true,
812
+ autoEditSize: true,
806
813
  supportsNumImages: true,
807
814
  supportsOutputFormat: true,
815
+ supportedOutputFormats: [
816
+ "jpeg",
817
+ "png",
818
+ "webp",
819
+ "gif",
820
+ "svg"
821
+ ],
822
+ supportedEditOutputFormats: [
823
+ "jpeg",
824
+ "png",
825
+ "webp",
826
+ "gif"
827
+ ],
808
828
  supportsQuality: true,
809
829
  supportedQualities: [
810
830
  "auto",
@@ -819,77 +839,6 @@ const MODELS = {
819
839
  type: "generation",
820
840
  useQueue: true
821
841
  },
822
- gpt2: {
823
- benchmark: {
824
- artificialAnalysis: {
825
- editing: {
826
- elo: 1125,
827
- rank: 5
828
- },
829
- snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
830
- sourceUrls: AA_IMAGE_SOURCES,
831
- textToImage: {
832
- elo: 1172,
833
- rank: 3
834
- }
835
- },
836
- speed: {
837
- medianSeconds: 200.7,
838
- p95Seconds: 232.6,
839
- source: "artificial-analysis-models"
840
- },
841
- tiers: {
842
- price: "ultra",
843
- quality: "frontier",
844
- speed: "very_slow"
845
- },
846
- useCase: "Established frontier generation and transparent PNGs"
847
- },
848
- customImageSize: {
849
- maxEdge: 3840,
850
- maxPixels: 8294400,
851
- maxRatio: 3,
852
- minPixels: 655360,
853
- multipleOf: 16
854
- },
855
- editEndpoint: "openai/gpt-image-2/edit",
856
- endpoint: "openai/gpt-image-2",
857
- falPricing: {
858
- checkedAt: FAL_PRICING_CHECKED_AT,
859
- currency: "USD",
860
- endpointId: "openai/gpt-image-2",
861
- estimatedCostPerImageUsd: .211,
862
- source: "fal-pricing-api",
863
- unit: "units",
864
- unitPrice: 1
865
- },
866
- maxReferenceImages: 16,
867
- name: "GPT Image 2",
868
- pricePerImageUsd: .211,
869
- pricing: "$0.211",
870
- sizeMode: "image_size_enum",
871
- supportsAspect: true,
872
- streaming: {
873
- generation: true,
874
- edit: true
875
- },
876
- supportsEdit: true,
877
- supportsMaskImage: true,
878
- maskImageField: "mask_url",
879
- supportsNumImages: true,
880
- supportsOutputFormat: true,
881
- supportsQuality: true,
882
- supportsResolution: false,
883
- supportsSyncMode: true,
884
- transparencyRoute: {
885
- apiKeyEnv: "OPENAI_API_KEY",
886
- model: "gpt-image-2",
887
- provider: "openai",
888
- supportsEdit: true
889
- },
890
- type: "generation",
891
- useQueue: true
892
- },
893
842
  gpt: {
894
843
  benchmark: {
895
844
  artificialAnalysis: {
@@ -2471,7 +2420,6 @@ const MODELS = {
2471
2420
  const GENERATION_MODELS = [
2472
2421
  "sunburst",
2473
2422
  "flare",
2474
- "gpt2",
2475
2423
  "gpt",
2476
2424
  "banana21",
2477
2425
  "banana2",
@@ -2536,7 +2484,6 @@ function estimateCost(model, resolution, numImages = 1, editImages = 0) {
2536
2484
  return configuredPrice * numImages;
2537
2485
  }
2538
2486
  switch (model) {
2539
- case "gpt2": return .211 * numImages;
2540
2487
  case "gpt": return .133 * numImages;
2541
2488
  case "banana":
2542
2489
  case "gemini3": return (resolution === "4K" ? .3 : .15) * numImages;
@@ -2565,7 +2512,7 @@ function estimateVideoCost(durationSeconds = 5, generateAudio = true, model = "k
2565
2512
  * `Response.json()` yields `any`; these helpers narrow that untyped payload into
2566
2513
  * the SDK's response types with runtime guards instead of unchecked assertions.
2567
2514
  */
2568
- function isRecord$4(value) {
2515
+ function isRecord$1(value) {
2569
2516
  return typeof value === "object" && value !== null;
2570
2517
  }
2571
2518
  function asString(value) {
@@ -2575,7 +2522,7 @@ function asNumber(value) {
2575
2522
  return typeof value === "number" ? value : void 0;
2576
2523
  }
2577
2524
  function toMotifImage(value) {
2578
- if (isRecord$4(value)) return {
2525
+ if (isRecord$1(value)) return {
2579
2526
  content_type: asString(value.content_type),
2580
2527
  height: asNumber(value.height),
2581
2528
  url: asString(value.url) ?? "",
@@ -2590,7 +2537,7 @@ function parseImages(value) {
2590
2537
  function parseLogs(value) {
2591
2538
  if (!Array.isArray(value)) return;
2592
2539
  const entries = [];
2593
- for (const entry of value) if (isRecord$4(entry)) entries.push({
2540
+ for (const entry of value) if (isRecord$1(entry)) entries.push({
2594
2541
  message: asString(entry.message) ?? "",
2595
2542
  timestamp: asString(entry.timestamp) ?? ""
2596
2543
  });
@@ -2620,7 +2567,7 @@ function queueAppPath(endpoint) {
2620
2567
  return endpoint.split("/").slice(0, 2).join("/");
2621
2568
  }
2622
2569
  function parseQueueSubmission(data) {
2623
- if (!isRecord$4(data)) return { requestId: "" };
2570
+ if (!isRecord$1(data)) return { requestId: "" };
2624
2571
  return {
2625
2572
  requestId: asString(data.request_id) ?? "",
2626
2573
  responseUrl: asString(data.response_url)
@@ -2660,7 +2607,7 @@ function requestIdFromBody(text) {
2660
2607
  } catch {
2661
2608
  return;
2662
2609
  }
2663
- if (!isRecord$4(parsed)) return;
2610
+ if (!isRecord$1(parsed)) return;
2664
2611
  return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
2665
2612
  }
2666
2613
  //#endregion
@@ -2673,6 +2620,18 @@ function requestIdFromBody(text) {
2673
2620
  * error can name both what a model does support and which models support the
2674
2621
  * option that was refused.
2675
2622
  */
2623
+ const DEFAULT_OUTPUT_FORMATS = [
2624
+ "jpeg",
2625
+ "png",
2626
+ "webp"
2627
+ ];
2628
+ /** The output formats a Model returns, for an edit when `editing` is set.
2629
+ * A Model that takes no format returns none it can be asked for. */
2630
+ function outputFormatsOf(config, editing) {
2631
+ if (config.supportsOutputFormat !== true) return [];
2632
+ const formats = config.supportedOutputFormats ?? DEFAULT_OUTPUT_FORMATS;
2633
+ return editing ? config.supportedEditOutputFormats ?? formats : formats;
2634
+ }
2676
2635
  /** Generation options a model can refuse, with the registry test for each. */
2677
2636
  const OPTION_CAPABILITIES = {
2678
2637
  aspect: (config) => (config.sizeMode ?? "aspect_ratio") !== "none",
@@ -2688,6 +2647,7 @@ const OPTION_CAPABILITIES = {
2688
2647
  imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
2689
2648
  inputFidelity: (config) => config.sizeMode === "gpt_size",
2690
2649
  limitGenerations: (config) => config.supportsLimitGenerations === true,
2650
+ markupImageUrl: (config) => config.supportsMarkupImage === true,
2691
2651
  maskImageUrl: (config) => config.supportsMaskImage === true,
2692
2652
  negativePrompt: (config) => config.supportsNegativePrompt === true,
2693
2653
  numImages: (config) => config.supportsNumImages,
@@ -2737,8 +2697,7 @@ var UnsupportedOptionError = class extends Error {
2737
2697
  constructor(config, option) {
2738
2698
  const models = modelsSupporting(option);
2739
2699
  const supported = supportedOptions(config);
2740
- const route = config.transparencyRoute !== void 0 && (option === "transparent output" || option === "background") ? ` ${config.name} does produce transparent PNGs through the ${config.transparencyRoute.provider} route (${config.transparencyRoute.apiKeyEnv}).` : "";
2741
- super(`${config.name} does not support ${option}. Models that do: ${models.length > 0 ? models.join(", ") : "none"}. ${config.name} supports: ${supported.length > 0 ? supported.join(", ") : "none of the optional settings"}.${route}`);
2700
+ super(`${config.name} does not support ${option}. Models that do: ${models.length > 0 ? models.join(", ") : "none"}. ${config.name} supports: ${supported.length > 0 ? supported.join(", ") : "none of the optional settings"}.`);
2742
2701
  this.name = "UnsupportedOptionError";
2743
2702
  this.model = config.name;
2744
2703
  this.modelsSupporting = models;
@@ -2821,6 +2780,9 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2821
2780
  if (options.imagePromptStrength !== void 0 && !hasEditImages) throw new Error("imagePromptStrength requires editImageUrls");
2822
2781
  if (options.maskImageUrl !== void 0 && config.supportsMaskImage !== true) unsupported(config, "maskImageUrl");
2823
2782
  if (options.maskImageUrl !== void 0 && !hasEditImages) throw new Error("maskImageUrl requires editImageUrls");
2783
+ if (options.markupImageUrl !== void 0 && config.supportsMarkupImage !== true) unsupported(config, "markupImageUrl");
2784
+ if (options.markupImageUrl !== void 0 && !hasEditImages) throw new Error("markupImageUrl requires editImageUrls");
2785
+ if (options.markupImageUrl !== void 0 && options.maskImageUrl !== void 0) throw new Error("A markup image can't be combined with a mask.");
2824
2786
  if (options.seed !== void 0 && config.supportsSeed !== true) unsupported(config, "seed");
2825
2787
  if (options.outputFormat !== void 0 && config.supportsOutputFormat !== true) unsupported(config, "outputFormat");
2826
2788
  if (options.quality !== void 0 && config.supportsQuality !== true) unsupported(config, "quality");
@@ -2856,11 +2818,11 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2856
2818
  * Models use different sizing APIs:
2857
2819
  * - `gpt_size`: GPT's fixed dimensions (1024x1024, 1536x1024, etc.)
2858
2820
  * - `aspect_ratio`: string like "16:9" (Gemini, Grok, FLUX Pro Ultra)
2859
- * - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image 2)
2821
+ * - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image)
2860
2822
  * - `none`: no size control (video models)
2861
2823
  */
2862
2824
  function buildGenerateBody(options) {
2863
- const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize, imagePromptStrength, limitGenerations, maskImageUrl, seed, outputFormat, quality, negativePrompt, style, syncMode, thinkingLevel, renderingSpeed, guidanceScale, numInferenceSteps, raw, enhancePrompt, safetyTolerance, enableWebSearch, expandPrompt } = options;
2825
+ const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize, imagePromptStrength, limitGenerations, maskImageUrl, markupImageUrl, seed, outputFormat, quality, negativePrompt, style, syncMode, thinkingLevel, renderingSpeed, guidanceScale, numInferenceSteps, raw, enhancePrompt, safetyTolerance, enableWebSearch, expandPrompt } = options;
2864
2826
  const config = MODELS[model];
2865
2827
  if (!config) throw new Error(`Unknown model: ${model}`);
2866
2828
  let { endpoint } = config;
@@ -2879,7 +2841,7 @@ function buildGenerateBody(options) {
2879
2841
  body.image_size = normalizedImageSize ?? (hasEditImages && !explicitAspect ? "auto" : aspectToGptSize(aspect));
2880
2842
  break;
2881
2843
  case "image_size_enum":
2882
- body.image_size = normalizedImageSize ?? falImageSizeFor(config, aspect, resolution);
2844
+ body.image_size = normalizedImageSize ?? (config.autoEditSize === true && hasEditImages && !explicitAspect ? "auto" : falImageSizeFor(config, aspect, resolution));
2883
2845
  break;
2884
2846
  case "aspect_ratio":
2885
2847
  if (config.supportsAspect) body.aspect_ratio = aspect;
@@ -2892,7 +2854,8 @@ function buildGenerateBody(options) {
2892
2854
  if (syncMode !== void 0 && config.supportsSyncMode === true) body.sync_mode = syncMode;
2893
2855
  if (seed !== void 0 && config.supportsSeed === true) body.seed = seed;
2894
2856
  if (outputFormat && config.supportsOutputFormat === true && transparent !== true) {
2895
- if (config.supportedOutputFormats && !config.supportedOutputFormats.includes(outputFormat)) throw new Error(`${config.name} supports output formats: ${config.supportedOutputFormats.join(", ")}`);
2857
+ const formats = outputFormatsOf(config, hasEditImages);
2858
+ if (!formats.includes(outputFormat)) throw new Error(`${config.name} ${hasEditImages ? "edits support" : "supports"} output formats: ${formats.join(", ")}`);
2896
2859
  body.output_format = outputFormat;
2897
2860
  }
2898
2861
  if (config.supportsRaw === true && raw !== void 0) body.raw = raw;
@@ -2951,6 +2914,7 @@ function buildGenerateBody(options) {
2951
2914
  if (inputFidelity) body.input_fidelity = inputFidelity;
2952
2915
  }
2953
2916
  if (maskImageUrl !== void 0 && maskImageUrl !== "" && config.supportsMaskImage === true) body[config.maskImageField ?? "mask_image_url"] = maskImageUrl;
2917
+ if (markupImageUrl !== void 0 && config.supportsMarkupImage === true) body.markup_image_url = markupImageUrl;
2954
2918
  return {
2955
2919
  body,
2956
2920
  endpoint
@@ -4508,7 +4472,7 @@ async function runRequest(exec, prepared) {
4508
4472
  const response = await exec.request(`${FAL_BASE_URL$1}/${prepared.endpoint}`, requestInit(prepared));
4509
4473
  if (response.isErr()) return (0, neverthrow.err)(response.error);
4510
4474
  const data = await response.value.json();
4511
- const record = isRecord$4(data) ? data : {};
4475
+ const record = isRecord$1(data) ? data : {};
4512
4476
  const requestId = response.value.headers.get("x-fal-request-id") ?? asString(record.request_id);
4513
4477
  return (0, neverthrow.ok)({
4514
4478
  data: record,
@@ -4535,14 +4499,19 @@ async function getToolResult(exec, job) {
4535
4499
  const response = await exec.request(url);
4536
4500
  if (response.isErr()) return (0, neverthrow.err)(response.error);
4537
4501
  const data = await response.value.json();
4538
- return (0, neverthrow.ok)(isRecord$4(data) ? data : {});
4502
+ return (0, neverthrow.ok)(isRecord$1(data) ? data : {});
4539
4503
  }
4540
4504
  /** Poll a submitted job to completion and fetch its result. Submits nothing. */
4541
4505
  async function awaitQueuedRequest(exec, job, onProgress) {
4506
+ let logsSeen = 0;
4542
4507
  for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
4543
4508
  const status = await checkToolStatus(exec, job);
4544
4509
  if (status.isErr()) return (0, neverthrow.err)(status.error);
4545
- onProgress?.(status.value.status, status.value.queuePosition);
4510
+ const logs = status.value.logs ?? [];
4511
+ const fresh = logs.slice(logs.length < logsSeen ? 0 : logsSeen);
4512
+ logsSeen = logs.length;
4513
+ if (fresh.length > 0) onProgress?.(status.value.status, status.value.queuePosition, fresh);
4514
+ else onProgress?.(status.value.status, status.value.queuePosition);
4546
4515
  if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
4547
4516
  data,
4548
4517
  requestId: job.requestId
@@ -4639,7 +4608,7 @@ var FalClient = class {
4639
4608
  try {
4640
4609
  built = buildGenerateBody(options);
4641
4610
  } catch (error) {
4642
- return (0, neverthrow.err)(toMotifError$1(error));
4611
+ return (0, neverthrow.err)(toMotifError(error));
4643
4612
  }
4644
4613
  const { endpoint, body } = built;
4645
4614
  const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
@@ -4677,7 +4646,7 @@ var FalClient = class {
4677
4646
  try {
4678
4647
  built = buildGenerateBody(options);
4679
4648
  } catch (error) {
4680
- return (0, neverthrow.err)(toMotifError$1(error));
4649
+ return (0, neverthrow.err)(toMotifError(error));
4681
4650
  }
4682
4651
  const { endpoint, body } = built;
4683
4652
  const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
@@ -4699,7 +4668,7 @@ var FalClient = class {
4699
4668
  const response = await this.request(url);
4700
4669
  if (response.isErr()) return (0, neverthrow.err)(response.error);
4701
4670
  const data = await response.value.json();
4702
- const record = isRecord$4(data) ? data : {};
4671
+ const record = isRecord$1(data) ? data : {};
4703
4672
  const rawStatus = asString(record.status);
4704
4673
  let status;
4705
4674
  if (rawStatus === "IN_QUEUE") status = "queued";
@@ -4807,7 +4776,7 @@ var FalClient = class {
4807
4776
  const response = await this.request(url);
4808
4777
  if (response.isErr()) return (0, neverthrow.err)(response.error);
4809
4778
  const data = await response.value.json();
4810
- const video = isRecord$4(data) && isRecord$4(data.video) ? data.video : void 0;
4779
+ const video = isRecord$1(data) && isRecord$1(data.video) ? data.video : void 0;
4811
4780
  if (video === void 0) return (0, neverthrow.err)(new MotifError("No video in response", 0));
4812
4781
  return (0, neverthrow.ok)({
4813
4782
  contentType: asString(video.content_type) ?? "",
@@ -4831,8 +4800,8 @@ var FalClient = class {
4831
4800
  });
4832
4801
  if (initiateResponse.isErr()) return (0, neverthrow.err)(initiateResponse.error);
4833
4802
  const initiateData = await initiateResponse.value.json();
4834
- const fileUrl = isRecord$4(initiateData) ? asString(initiateData.file_url) ?? "" : "";
4835
- const uploadUrl = isRecord$4(initiateData) ? asString(initiateData.upload_url) : void 0;
4803
+ const fileUrl = isRecord$1(initiateData) ? asString(initiateData.file_url) ?? "" : "";
4804
+ const uploadUrl = isRecord$1(initiateData) ? asString(initiateData.upload_url) : void 0;
4836
4805
  if (uploadUrl === void 0 || uploadUrl === "") return (0, neverthrow.err)(new MotifError("Upload initiate response missing upload_url", 0));
4837
4806
  let putResponse;
4838
4807
  try {
@@ -4984,7 +4953,7 @@ var FalClient = class {
4984
4953
  * Some APIs return `{ image: {...} }` instead of `{ images: [...] }`.
4985
4954
  */
4986
4955
  normalizeResponse(data, fallbackRequestId) {
4987
- if (!isRecord$4(data)) return (0, neverthrow.err)(new MotifError("Unexpected fal response shape", 0, "FAL_ERROR"));
4956
+ if (!isRecord$1(data)) return (0, neverthrow.err)(new MotifError("Unexpected fal response shape", 0, "FAL_ERROR"));
4988
4957
  const obj = data;
4989
4958
  const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
4990
4959
  if ("detail" in obj) return (0, neverthrow.err)(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
@@ -5287,6 +5256,188 @@ function sumCosts(costs) {
5287
5256
  };
5288
5257
  }
5289
5258
  //#endregion
5259
+ //#region src/image-benchmarks.ts
5260
+ const BOARDS = {
5261
+ "text-to-image": {
5262
+ sunburst: {
5263
+ elo: 1427,
5264
+ rank: 1,
5265
+ uncertainty: 5,
5266
+ evaluatedModel: "GPT-Image-2.5 Sunburst",
5267
+ preliminary: true,
5268
+ votes: 40323
5269
+ },
5270
+ flare: {
5271
+ elo: 1401,
5272
+ rank: 2,
5273
+ uncertainty: 5,
5274
+ evaluatedModel: "GPT-Image-2.5 Flare",
5275
+ preliminary: true,
5276
+ votes: 39218
5277
+ },
5278
+ banana21: {
5279
+ elo: 1325,
5280
+ rank: 5,
5281
+ uncertainty: 9,
5282
+ evaluatedModel: "Nano Banana 2.1",
5283
+ preliminary: true,
5284
+ votes: 5290
5285
+ },
5286
+ "grok-image-2": {
5287
+ elo: 1317,
5288
+ rank: 6,
5289
+ uncertainty: 7,
5290
+ evaluatedModel: "Grok Imagine Image 2.0",
5291
+ preliminary: true,
5292
+ votes: 10902
5293
+ },
5294
+ "muse-image": {
5295
+ elo: 1269,
5296
+ rank: 8,
5297
+ uncertainty: 5,
5298
+ evaluatedModel: "Muse Image (Meta)",
5299
+ votes: 53444
5300
+ },
5301
+ qwen3: {
5302
+ elo: 1254,
5303
+ rank: 9,
5304
+ uncertainty: 6,
5305
+ evaluatedModel: "Qwen-Image-3.0-Pro",
5306
+ votes: 11444
5307
+ },
5308
+ "mai-image-2.5": {
5309
+ elo: 1253,
5310
+ rank: 10,
5311
+ uncertainty: 4,
5312
+ evaluatedModel: "MAI-Image-2.5",
5313
+ votes: 61286
5314
+ },
5315
+ seedream5: {
5316
+ elo: 1250,
5317
+ rank: 11,
5318
+ uncertainty: 4,
5319
+ evaluatedModel: "Seedream 5.0 Pro",
5320
+ votes: 105824
5321
+ },
5322
+ "banana2-lite": {
5323
+ elo: 1249,
5324
+ rank: 12,
5325
+ uncertainty: 6,
5326
+ evaluatedModel: "Nano Banana 2 Lite",
5327
+ votes: 17813
5328
+ },
5329
+ banana: {
5330
+ elo: 1246,
5331
+ rank: 13,
5332
+ uncertainty: 3,
5333
+ evaluatedModel: "Nano Banana Pro (2K)",
5334
+ votes: 188288
5335
+ },
5336
+ gpt: {
5337
+ elo: 1236,
5338
+ rank: 14,
5339
+ uncertainty: 3,
5340
+ evaluatedModel: "GPT-Image-1.5 (High Fidelity)",
5341
+ votes: 174012
5342
+ },
5343
+ ideogram4: {
5344
+ elo: 1200,
5345
+ rank: 15,
5346
+ uncertainty: 4,
5347
+ evaluatedModel: "Ideogram 4.0 (Quality)",
5348
+ votes: 72865
5349
+ },
5350
+ "flux2-flex": {
5351
+ elo: 1156,
5352
+ rank: 17,
5353
+ uncertainty: 3,
5354
+ evaluatedModel: "FLUX.2 [flex]",
5355
+ votes: 156876
5356
+ }
5357
+ },
5358
+ "image-edit-single-image": {
5359
+ sunburst: {
5360
+ elo: 1517,
5361
+ rank: 1,
5362
+ uncertainty: 5,
5363
+ evaluatedModel: "GPT-Image-2.5 Sunburst",
5364
+ preliminary: true
5365
+ },
5366
+ flare: {
5367
+ elo: 1482,
5368
+ rank: 2,
5369
+ uncertainty: 5,
5370
+ evaluatedModel: "GPT-Image-2.5 Flare",
5371
+ preliminary: true
5372
+ },
5373
+ "grok-image-2": {
5374
+ elo: 1439,
5375
+ rank: 5,
5376
+ uncertainty: 6,
5377
+ evaluatedModel: "Grok Imagine Image 2.0",
5378
+ preliminary: true
5379
+ },
5380
+ banana21: {
5381
+ elo: 1427,
5382
+ rank: 7,
5383
+ uncertainty: 6,
5384
+ evaluatedModel: "Nano Banana 2.1",
5385
+ preliminary: true
5386
+ },
5387
+ "mai-image-2.5": {
5388
+ elo: 1400,
5389
+ rank: 9,
5390
+ uncertainty: 4,
5391
+ evaluatedModel: "MAI-Image-2.5"
5392
+ },
5393
+ "muse-image": {
5394
+ elo: 1400,
5395
+ rank: 10,
5396
+ uncertainty: 4,
5397
+ evaluatedModel: "Muse Image (Meta)"
5398
+ },
5399
+ "grok-image-quality": {
5400
+ elo: 1391,
5401
+ rank: 11,
5402
+ uncertainty: 6,
5403
+ evaluatedModel: "Grok Imagine Image (Quality, 20260519)"
5404
+ },
5405
+ seedream5: {
5406
+ elo: 1390,
5407
+ rank: 12,
5408
+ uncertainty: 3,
5409
+ evaluatedModel: "Seedream 5.0 Pro"
5410
+ },
5411
+ banana: {
5412
+ elo: 1390,
5413
+ rank: 13,
5414
+ uncertainty: 3,
5415
+ evaluatedModel: "Nano Banana Pro (2K)"
5416
+ },
5417
+ gpt: {
5418
+ elo: 1368,
5419
+ rank: 14,
5420
+ uncertainty: 3,
5421
+ evaluatedModel: "GPT-Image-1.5 (High Fidelity)"
5422
+ }
5423
+ }
5424
+ };
5425
+ const SOURCES = {
5426
+ "text-to-image": "https://arena.ai/leaderboard/image/text-to-image",
5427
+ "image-edit-single-image": "https://arena.ai/leaderboard/image/image-edit/single-image"
5428
+ };
5429
+ /** A published score for this route, or null when no matching score was saved. */
5430
+ function imageModelBenchmark(model, task) {
5431
+ const score = BOARDS[task][model];
5432
+ if (score === void 0) return null;
5433
+ return {
5434
+ ...score,
5435
+ source: SOURCES[task],
5436
+ snapshotDate: "2026-10-29",
5437
+ task
5438
+ };
5439
+ }
5440
+ //#endregion
5290
5441
  //#region src/tasks.ts
5291
5442
  /**
5292
5443
  * The Task registry: every job Motif does, with the Models that can do it
@@ -5314,6 +5465,186 @@ const TIERS = [
5314
5465
  const DEFAULT_TIER = "balanced";
5315
5466
  const HAND_RANKED_AT = "2026-09-16";
5316
5467
  const IMAGE_RANKED_AT = "2026-10-29";
5468
+ function withImageBenchmarks(models, task) {
5469
+ return models.map((entry) => ({
5470
+ ...entry,
5471
+ benchmark: imageModelBenchmark(entry.model, task)
5472
+ }));
5473
+ }
5474
+ const GENERATION_MODELS_RANKED = withImageBenchmarks([
5475
+ {
5476
+ model: "sunburst",
5477
+ tier: "quality"
5478
+ },
5479
+ {
5480
+ model: "flare",
5481
+ tier: "quality"
5482
+ },
5483
+ {
5484
+ model: "seedream5",
5485
+ tier: "quality"
5486
+ },
5487
+ {
5488
+ model: "gpt",
5489
+ tier: "quality"
5490
+ },
5491
+ {
5492
+ model: "mai-image-2.5-pro",
5493
+ tier: "quality"
5494
+ },
5495
+ {
5496
+ model: "banana21",
5497
+ tier: "balanced"
5498
+ },
5499
+ {
5500
+ model: "grok-image-2",
5501
+ tier: "balanced"
5502
+ },
5503
+ {
5504
+ model: "muse-image",
5505
+ tier: "balanced"
5506
+ },
5507
+ {
5508
+ model: "banana2",
5509
+ tier: "balanced"
5510
+ },
5511
+ {
5512
+ model: "qwen3",
5513
+ tier: "balanced"
5514
+ },
5515
+ {
5516
+ model: "mai-image-2.5",
5517
+ tier: "balanced"
5518
+ },
5519
+ {
5520
+ model: "banana",
5521
+ tier: "balanced"
5522
+ },
5523
+ {
5524
+ model: "ideogram4",
5525
+ tier: "balanced"
5526
+ },
5527
+ {
5528
+ model: "flux2-flex",
5529
+ tier: "balanced"
5530
+ },
5531
+ {
5532
+ model: "recraft41",
5533
+ tier: "balanced"
5534
+ },
5535
+ {
5536
+ model: "flux2-pro",
5537
+ tier: "balanced"
5538
+ },
5539
+ {
5540
+ model: "krea3",
5541
+ tier: "balanced"
5542
+ },
5543
+ {
5544
+ model: "grok-image-quality",
5545
+ tier: "balanced"
5546
+ },
5547
+ {
5548
+ model: "ideogram3-transparent",
5549
+ requires: ["transparency"],
5550
+ supports: ["transparency"],
5551
+ tier: "balanced"
5552
+ },
5553
+ {
5554
+ model: "banana2-lite",
5555
+ tier: "fast"
5556
+ },
5557
+ {
5558
+ model: "grok-image",
5559
+ tier: "fast"
5560
+ },
5561
+ {
5562
+ model: "seedream5-lite",
5563
+ tier: "fast"
5564
+ },
5565
+ {
5566
+ model: "flux2-turbo",
5567
+ tier: "fast"
5568
+ }
5569
+ ], "text-to-image");
5570
+ const EDITING_MODELS_RANKED = withImageBenchmarks([
5571
+ {
5572
+ model: "sunburst",
5573
+ tier: "quality"
5574
+ },
5575
+ {
5576
+ model: "flare",
5577
+ tier: "quality"
5578
+ },
5579
+ {
5580
+ model: "seedream5",
5581
+ tier: "quality"
5582
+ },
5583
+ {
5584
+ model: "gpt",
5585
+ tier: "quality"
5586
+ },
5587
+ {
5588
+ model: "mai-image-2.5-pro",
5589
+ tier: "quality"
5590
+ },
5591
+ {
5592
+ model: "grok-image-2",
5593
+ tier: "balanced"
5594
+ },
5595
+ {
5596
+ model: "banana21",
5597
+ tier: "balanced"
5598
+ },
5599
+ {
5600
+ model: "mai-image-2.5",
5601
+ tier: "balanced"
5602
+ },
5603
+ {
5604
+ model: "muse-image",
5605
+ tier: "balanced"
5606
+ },
5607
+ {
5608
+ model: "grok-image-quality",
5609
+ tier: "balanced"
5610
+ },
5611
+ {
5612
+ model: "banana",
5613
+ tier: "balanced"
5614
+ },
5615
+ {
5616
+ model: "banana2",
5617
+ tier: "balanced"
5618
+ },
5619
+ {
5620
+ model: "krea3",
5621
+ tier: "balanced"
5622
+ },
5623
+ {
5624
+ model: "qwen3",
5625
+ tier: "balanced"
5626
+ },
5627
+ {
5628
+ model: "flux2-pro",
5629
+ tier: "balanced"
5630
+ },
5631
+ {
5632
+ model: "flux2-flex",
5633
+ tier: "balanced"
5634
+ },
5635
+ {
5636
+ model: "grok-image",
5637
+ tier: "fast"
5638
+ },
5639
+ {
5640
+ model: "banana2-lite",
5641
+ tier: "fast"
5642
+ },
5643
+ {
5644
+ model: "seedream5-lite",
5645
+ tier: "fast"
5646
+ }
5647
+ ], "image-edit-single-image");
5317
5648
  /** Every Task with a Model. A Task is absent here until it has at least one Model. */
5318
5649
  const TASKS = {
5319
5650
  animate: {
@@ -5485,106 +5816,7 @@ const TASKS = {
5485
5816
  },
5486
5817
  generate: {
5487
5818
  basis: "Order follows Arena text-to-image (https://arena.ai/leaderboard/image/text-to-image), read 2026-10-29 (MOT-72). Sunburst and Flare lead quality at the benchmarked max setting. Balanced starts with Nano Banana 2.1 on preliminary Arena evidence, then Grok Image 2 and Muse Image. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Fast keeps Nano Banana 2 Lite, budget Grok and Seedream 5 Lite, and FLUX.2 Turbo. Design, transparency and tuned Look routes are capability exceptions. Generation with References uses the separate editing ranking. Older routes are explicit/pin/Look-only.",
5488
- models: [
5489
- {
5490
- model: "sunburst",
5491
- tier: "quality"
5492
- },
5493
- {
5494
- model: "flare",
5495
- tier: "quality"
5496
- },
5497
- {
5498
- model: "gpt2",
5499
- tier: "quality"
5500
- },
5501
- {
5502
- model: "seedream5",
5503
- tier: "quality"
5504
- },
5505
- {
5506
- model: "gpt",
5507
- tier: "quality"
5508
- },
5509
- {
5510
- model: "mai-image-2.5-pro",
5511
- tier: "quality"
5512
- },
5513
- {
5514
- model: "banana21",
5515
- tier: "balanced"
5516
- },
5517
- {
5518
- model: "grok-image-2",
5519
- tier: "balanced"
5520
- },
5521
- {
5522
- model: "muse-image",
5523
- tier: "balanced"
5524
- },
5525
- {
5526
- model: "banana2",
5527
- tier: "balanced"
5528
- },
5529
- {
5530
- model: "qwen3",
5531
- tier: "balanced"
5532
- },
5533
- {
5534
- model: "mai-image-2.5",
5535
- tier: "balanced"
5536
- },
5537
- {
5538
- model: "banana",
5539
- tier: "balanced"
5540
- },
5541
- {
5542
- model: "ideogram4",
5543
- tier: "balanced"
5544
- },
5545
- {
5546
- model: "flux2-flex",
5547
- tier: "balanced"
5548
- },
5549
- {
5550
- model: "recraft41",
5551
- tier: "balanced"
5552
- },
5553
- {
5554
- model: "flux2-pro",
5555
- tier: "balanced"
5556
- },
5557
- {
5558
- model: "krea3",
5559
- tier: "balanced"
5560
- },
5561
- {
5562
- model: "grok-image-quality",
5563
- tier: "balanced"
5564
- },
5565
- {
5566
- model: "ideogram3-transparent",
5567
- requires: ["transparency"],
5568
- supports: ["transparency"],
5569
- tier: "balanced"
5570
- },
5571
- {
5572
- model: "banana2-lite",
5573
- tier: "fast"
5574
- },
5575
- {
5576
- model: "grok-image",
5577
- tier: "fast"
5578
- },
5579
- {
5580
- model: "seedream5-lite",
5581
- tier: "fast"
5582
- },
5583
- {
5584
- model: "flux2-turbo",
5585
- tier: "fast"
5586
- }
5587
- ],
5819
+ models: GENERATION_MODELS_RANKED,
5588
5820
  notFor: "Variations of an image you already have (vary), or a consistent set of images (series run).",
5589
5821
  rankedAt: IMAGE_RANKED_AT,
5590
5822
  rankedFrom: "hand",
@@ -6072,88 +6304,7 @@ const TASKS = {
6072
6304
  },
6073
6305
  vary: {
6074
6306
  basis: "Order follows Arena image edit, single image (https://arena.ai/leaderboard/image/image-edit/single-image), read 2026-10-29 (MOT-72). Editing order differs from generation: Grok Image 2 leads balanced, ahead of Nano Banana 2.1, and Seedream 5 Pro outranks GPT Image 1.5. Arena scores Krea 3 only at its Ultra setting, which costs $0.27 to $0.35 an edit and ignores the requested size, so krea3, which runs fal's default setting, is ranked as unscored. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Sunburst and Flare use max quality. vary reuses a previously chosen Model when explicitly supplied, including retained legacy routes.",
6075
- models: [
6076
- {
6077
- model: "sunburst",
6078
- tier: "quality"
6079
- },
6080
- {
6081
- model: "flare",
6082
- tier: "quality"
6083
- },
6084
- {
6085
- model: "gpt2",
6086
- tier: "quality"
6087
- },
6088
- {
6089
- model: "seedream5",
6090
- tier: "quality"
6091
- },
6092
- {
6093
- model: "gpt",
6094
- tier: "quality"
6095
- },
6096
- {
6097
- model: "mai-image-2.5-pro",
6098
- tier: "quality"
6099
- },
6100
- {
6101
- model: "grok-image-2",
6102
- tier: "balanced"
6103
- },
6104
- {
6105
- model: "banana21",
6106
- tier: "balanced"
6107
- },
6108
- {
6109
- model: "mai-image-2.5",
6110
- tier: "balanced"
6111
- },
6112
- {
6113
- model: "muse-image",
6114
- tier: "balanced"
6115
- },
6116
- {
6117
- model: "grok-image-quality",
6118
- tier: "balanced"
6119
- },
6120
- {
6121
- model: "banana",
6122
- tier: "balanced"
6123
- },
6124
- {
6125
- model: "banana2",
6126
- tier: "balanced"
6127
- },
6128
- {
6129
- model: "krea3",
6130
- tier: "balanced"
6131
- },
6132
- {
6133
- model: "qwen3",
6134
- tier: "balanced"
6135
- },
6136
- {
6137
- model: "flux2-pro",
6138
- tier: "balanced"
6139
- },
6140
- {
6141
- model: "flux2-flex",
6142
- tier: "balanced"
6143
- },
6144
- {
6145
- model: "grok-image",
6146
- tier: "fast"
6147
- },
6148
- {
6149
- model: "banana2-lite",
6150
- tier: "fast"
6151
- },
6152
- {
6153
- model: "seedream5-lite",
6154
- tier: "fast"
6155
- }
6156
- ],
6307
+ models: EDITING_MODELS_RANKED,
6157
6308
  notFor: "A specific change to an image described in words (generate with a reference), or a set of different scenes in one style (series run).",
6158
6309
  rankedAt: IMAGE_RANKED_AT,
6159
6310
  rankedFrom: "hand",
@@ -8347,11 +8498,10 @@ const NO_MODEL_AVAILABLE = "NO_MODEL_AVAILABLE";
8347
8498
  const FAL_KEY = "FAL_KEY";
8348
8499
  function generationProfile(config) {
8349
8500
  const capabilities = /* @__PURE__ */ new Set(["image"]);
8350
- const keyed = /* @__PURE__ */ new Map();
8351
8501
  if (config.supportsBackground === true || config.transparentOutput === true) capabilities.add("transparency");
8352
- else if (config.transparencyRoute !== void 0) keyed.set("transparency", config.transparencyRoute.apiKeyEnv);
8353
8502
  if (config.supportsEdit) capabilities.add("references");
8354
8503
  if (config.supportsMaskImage === true) capabilities.add("mask");
8504
+ if (config.supportsMarkupImage === true) capabilities.add("markup");
8355
8505
  if ((config.sizeMode ?? "aspect_ratio") !== "none") capabilities.add("aspect");
8356
8506
  if (config.supportsResolution) capabilities.add("resolution");
8357
8507
  if (config.supportsNumImages) capabilities.add("count");
@@ -8361,9 +8511,12 @@ function generationProfile(config) {
8361
8511
  return {
8362
8512
  aspects: config.supportedAspects,
8363
8513
  capabilities,
8364
- keyedCapabilities: keyed,
8365
8514
  keys: [FAL_KEY],
8366
8515
  maxReferences: config.supportsEdit ? config.maxReferenceImages ?? 1 : 0,
8516
+ outputFormats: {
8517
+ edit: outputFormatsOf(config, true),
8518
+ text: outputFormatsOf(config, false)
8519
+ },
8367
8520
  resolutions: config.supportedResolutions
8368
8521
  };
8369
8522
  }
@@ -8390,7 +8543,6 @@ function toolProfile(tool) {
8390
8543
  if (takesReference) capabilities.add("references");
8391
8544
  return {
8392
8545
  capabilities,
8393
- keyedCapabilities: /* @__PURE__ */ new Map(),
8394
8546
  keys: [FAL_KEY],
8395
8547
  maxReferences: takesReference ? 1 : 0
8396
8548
  };
@@ -8414,12 +8566,13 @@ function requirements(request) {
8414
8566
  if (request.transparent === true) needed.push("transparency");
8415
8567
  if ((request.references ?? 0) > 0) needed.push("references");
8416
8568
  if (request.mask === true) needed.push("mask");
8569
+ if (request.markup === true) needed.push("markup");
8417
8570
  if (request.aspect !== void 0) needed.push("aspect");
8418
8571
  if (request.resolution !== void 0) needed.push("resolution");
8419
8572
  if ((request.count ?? 1) > 1) needed.push("count");
8420
8573
  if (request.seed === true) needed.push("seed");
8421
8574
  if (request.negativePrompt === true) needed.push("negativePrompt");
8422
- if (request.outputFormat === true) needed.push("outputFormat");
8575
+ if (request.outputFormat !== void 0) needed.push("outputFormat");
8423
8576
  if (request.rig === true) needed.push("rig");
8424
8577
  return needed;
8425
8578
  }
@@ -8428,6 +8581,7 @@ function withinLimits(profile, capability, request) {
8428
8581
  if (capability === "references") return (request.references ?? 0) <= profile.maxReferences;
8429
8582
  if (capability === "aspect") return accepts(profile.aspects, request.aspect);
8430
8583
  if (capability === "resolution") return accepts(profile.resolutions, request.resolution);
8584
+ if (capability === "outputFormat") return accepts((request.references ?? 0) > 0 || request.source !== void 0 ? profile.outputFormats?.edit : profile.outputFormats?.text, request.outputFormat);
8431
8585
  return true;
8432
8586
  }
8433
8587
  function accepts(allowed, value) {
@@ -8444,25 +8598,10 @@ function check(profile, needed, request, keys, requires = []) {
8444
8598
  missingInput: true,
8445
8599
  ok: false
8446
8600
  };
8447
- for (const capability of needed) {
8448
- if (profile.capabilities.has(capability)) {
8449
- if (!withinLimits(profile, capability, request)) return {
8450
- blockedBy: capability,
8451
- ok: false
8452
- };
8453
- continue;
8454
- }
8455
- const key = profile.keyedCapabilities.get(capability);
8456
- if (key === void 0) return {
8457
- blockedBy: capability,
8458
- ok: false
8459
- };
8460
- if (!keys.includes(key)) return {
8461
- blockedBy: "key",
8462
- missingKey: key,
8463
- ok: false
8464
- };
8465
- }
8601
+ for (const capability of needed) if (!profile.capabilities.has(capability) || !withinLimits(profile, capability, request)) return {
8602
+ blockedBy: capability,
8603
+ ok: false
8604
+ };
8466
8605
  return { ok: true };
8467
8606
  }
8468
8607
  const TIER_ORDER = {
@@ -8482,7 +8621,11 @@ const TIER_ORDER = {
8482
8621
  "fast"
8483
8622
  ]
8484
8623
  };
8485
- function describeBlock(blockedBy, missingKey, missingInput) {
8624
+ function describeBlock(blockedBy, request, missingKey, missingInput) {
8625
+ if (blockedBy === "outputFormat" && request.outputFormat !== void 0) {
8626
+ const editing = (request.references ?? 0) > 0 || request.source !== void 0;
8627
+ return `cannot return ${request.outputFormat}${editing ? " from an edit" : ""}`;
8628
+ }
8486
8629
  if (missingInput === true) return `needs a ${blockedBy} input`;
8487
8630
  if (blockedBy === "key") return `needs ${missingKey ?? "a provider key"}`;
8488
8631
  if (blockedBy === "unknown-model") return "is not a Model for this Task";
@@ -8551,7 +8694,7 @@ function resolveFixed(task, fixed, request, needed, keys) {
8551
8694
  if (result.ok) return resolved(task, model, tier, chosenBy);
8552
8695
  const unblockers = chosenBy === "model" ? [] : ["model"];
8553
8696
  unblockers.push(unblockerFor(result));
8554
- return unresolved(task, result.blockedBy, unblockers, `${model} (${chosenBy}) ${describeBlock(result.blockedBy, result.missingKey, result.missingInput)}.`, result.missingKey);
8697
+ return unresolved(task, result.blockedBy, unblockers, `${model} (${chosenBy}) ${describeBlock(result.blockedBy, request, result.missingKey, result.missingInput)}.`, result.missingKey);
8555
8698
  }
8556
8699
  /** Old pins and tuned Looks remain usable without recommending predecessors. */
8557
8700
  function legacyEntry(task, model, mode, tier) {
@@ -8648,730 +8791,6 @@ function tierChangesChoice(task, request, environment) {
8648
8791
  return chosen.size > 1;
8649
8792
  }
8650
8793
  //#endregion
8651
- //#region src/image/fetch.ts
8652
- function urlOf$1(input) {
8653
- if (typeof input === "string") return input;
8654
- return input instanceof URL ? input.href : input.url;
8655
- }
8656
- /** Adapt a configured fetch for a provider factory; undefined keeps global fetch. */
8657
- function toProviderFetch(fetch) {
8658
- if (fetch === void 0) return {};
8659
- return { fetch: async (input, init) => await fetch(urlOf$1(input), init ?? {}) };
8660
- }
8661
- //#endregion
8662
- //#region src/image/fal.ts
8663
- /**
8664
- * fal provider adapter.
8665
- *
8666
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/fal`. Building a model
8667
- * performs no network I/O — the request only happens when `generateImage`
8668
- * invokes `model.doGenerate`. This is the lightweight fal *image* adapter; the
8669
- * richer `FalClient` fal client (queue, upload, upscale, rmbg, video, tools)
8670
- * is a separate, later fold (design doc §8, phase 1d).
8671
- *
8672
- * ENDPOINT QUIRK (Phase 0): fal's gpt-image endpoint wants `image_size` as a
8673
- * STRING enum (e.g. "1024x1024"), passed at generate time via
8674
- * `providerOptions.fal.image_size` — NOT the AI SDK's generic `size` object.
8675
- * This adapter does NOT auto-inject it; callers pass `providerOptions` when they
8676
- * need a specific size. Example:
8677
- * img.generate({
8678
- * provider: "fal",
8679
- * model: "fal-ai/gpt-image-1.5",
8680
- * prompt: "...",
8681
- * providerOptions: { fal: { image_size: "1024x1024" } },
8682
- * });
8683
- */
8684
- /**
8685
- * fal endpoint ids proven to work with this adapter and priced in
8686
- * {@link FAL_IMAGE_PRICE_USD} below (FLUX Pro Ultra + gpt-image).
8687
- */
8688
- const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
8689
- const FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
8690
- /** Env var read for the fal key when `apiKey` is not supplied in config. */
8691
- const FAL_API_KEY_ENV = "FAL_KEY";
8692
- /**
8693
- * Static fal USD/image, keyed by the fal ENDPOINT id. Cost tracking must price
8694
- * the endpoints consumers actually pass — kiln uses
8695
- * `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
8696
- * sourced from the fal registry snapshot in `../models`
8697
- * (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
8698
- * drifting to a second hand-maintained copy; the `??` fallbacks are the registry
8699
- * values at time of writing, used only if a registry entry loses its optional
8700
- * price. Each key is `MODELS[x].endpoint`; the trailing comment cites the
8701
- * `MODELS` key it comes from. Covers the flux/flux2 family, banana, seedream, and
8702
- * the other priced fal generation endpoints.
8703
- */
8704
- const FAL_IMAGE_PRICE_USD = {
8705
- [FAL_FLUX_MODEL]: MODELS.flux?.pricePerImageUsd ?? .06,
8706
- "fal-ai/flux/schnell": MODELS["flux-fast"]?.pricePerImageUsd ?? .003,
8707
- "fal-ai/flux-2-max": MODELS["flux2-max"]?.pricePerImageUsd ?? .07,
8708
- "fal-ai/flux-2-pro": MODELS["flux2-pro"]?.pricePerImageUsd ?? .03,
8709
- "fal-ai/flux-2-flex": MODELS["flux2-flex"]?.pricePerImageUsd ?? .05,
8710
- "fal-ai/flux-2": MODELS["flux2-dev"]?.pricePerImageUsd ?? .012,
8711
- "fal-ai/flux-2/turbo": MODELS["flux2-turbo"]?.pricePerImageUsd ?? .008,
8712
- [FAL_GPT_IMAGE_MODEL]: MODELS.gpt?.pricePerImageUsd ?? .133,
8713
- "openai/gpt-image-2": MODELS.gpt2?.pricePerImageUsd ?? .211,
8714
- "fal-ai/nano-banana-2": MODELS.banana2?.pricePerImageUsd ?? .08,
8715
- "fal-ai/nano-banana-pro": MODELS.banana?.pricePerImageUsd ?? .15,
8716
- "fal-ai/gemini-25-flash-image": MODELS.gemini?.pricePerImageUsd ?? .0398,
8717
- "fal-ai/gemini-3-pro-image-preview": MODELS.gemini3?.pricePerImageUsd ?? .15,
8718
- "fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
8719
- "fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
8720
- "bytedance/seedream/v5/pro/text-to-image": MODELS.seedream5?.pricePerImageUsd ?? .0675,
8721
- "bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
8722
- "fal-ai/recraft/v3/text-to-image": MODELS.recraft?.pricePerImageUsd ?? .04,
8723
- "fal-ai/recraft/v4/text-to-image": MODELS.recraft4?.pricePerImageUsd ?? .04,
8724
- "fal-ai/recraft/v4.1/text-to-image": MODELS.recraft41?.pricePerImageUsd ?? .035,
8725
- "fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
8726
- "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
8727
- "xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
8728
- "xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
8729
- "xai/grok-imagine-image/quality/text-to-image": MODELS["grok-image-quality"]?.pricePerImageUsd ?? .05,
8730
- "meta/muse-image/text-to-image": MODELS["muse-image"]?.pricePerImageUsd ?? .01,
8731
- "microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
8732
- "fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
8733
- "alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
8734
- "fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
8735
- };
8736
- /**
8737
- * Build a fal `ImageModel`. Prefers the passed `apiKey`, else the `FAL_KEY` env
8738
- * var. Throws `MotifError` when neither is present (callers translate this into
8739
- * a `Result.err`).
8740
- */
8741
- function resolveModel$3(modelId, apiKey, fetch) {
8742
- const key = apiKey ?? process.env["FAL_KEY"];
8743
- if (key === void 0 || key === "") throw new MotifError(`fal image generation requires an API key (config.fal.apiKey or ${FAL_API_KEY_ENV})`, 0);
8744
- return (0, _ai_sdk_fal.createFal)({
8745
- apiKey: key,
8746
- ...toProviderFetch(fetch)
8747
- }).image(modelId);
8748
- }
8749
- /**
8750
- * How a registered fal edit endpoint takes its input images, from the
8751
- * registry's `editImagesField` (default `image_urls`). `@ai-sdk/fal` sends only
8752
- * the first image as `image_url` unless told otherwise, which list-only
8753
- * endpoints reject and which silently drops every reference after the first.
8754
- * Undefined for an endpoint the registry does not list as an edit route.
8755
- */
8756
- function falEditImagesField(modelId) {
8757
- const config = Object.values(MODELS).find((entry) => entry.editEndpoint === modelId);
8758
- return config === void 0 ? void 0 : config.editImagesField ?? "image_urls";
8759
- }
8760
- /** The fal provider adapter registered in the provider registry. */
8761
- const falAdapter = {
8762
- id: "fal",
8763
- apiKeyEnv: FAL_API_KEY_ENV,
8764
- resolveModel: resolveModel$3,
8765
- priceUsdByModel: FAL_IMAGE_PRICE_USD
8766
- };
8767
- //#endregion
8768
- //#region src/image/openai.ts
8769
- /**
8770
- * OpenAI (gpt-image) provider adapter.
8771
- *
8772
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/openai`. Building a model
8773
- * performs no network I/O — the request only happens when `generateImage`
8774
- * invokes `model.doGenerate`.
8775
- */
8776
- /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
8777
- const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
8778
- /**
8779
- * Static OpenAI-direct USD/image, keyed by model id.
8780
- *
8781
- * `gpt-image-1` ≈ $0.042/image at medium quality, 1024×1024. OpenAI bills image
8782
- * output by tokens, so the real figure varies with quality (low/medium/high)
8783
- * and size — this is a documented approximation for the common case.
8784
- * Source: https://platform.openai.com/docs/pricing (image generation)
8785
- * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
8786
- * (vs $0.133 via fal — full data and methodology on Linear MOT-23).
8787
- *
8788
- * GPT Image 2.5 is token-priced, with no published per-image estimate. Leave
8789
- * these models absent so cost remains unknown unless supplied by the provider.
8790
- * https://developers.openai.com/api/docs/models/gpt-image-2.5-flare
8791
- * https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst
8792
- */
8793
- const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
8794
- /**
8795
- * Build an OpenAI `ImageModel`. Prefers the passed `apiKey`, else the
8796
- * `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
8797
- * (callers translate this into a `Result.err`).
8798
- */
8799
- function resolveModel$2(modelId, apiKey, fetch) {
8800
- const key = apiKey ?? process.env["OPENAI_API_KEY"];
8801
- if (key === void 0 || key === "") throw new MotifError(`OpenAI image generation requires an API key (config.openai.apiKey or ${OPENAI_API_KEY_ENV})`, 0);
8802
- return (0, _ai_sdk_openai.createOpenAI)({
8803
- apiKey: key,
8804
- ...toProviderFetch(fetch)
8805
- }).image(modelId);
8806
- }
8807
- /** The OpenAI provider adapter registered in the provider registry. */
8808
- const openaiAdapter = {
8809
- id: "openai",
8810
- apiKeyEnv: OPENAI_API_KEY_ENV,
8811
- resolveModel: resolveModel$2,
8812
- priceUsdByModel: OPENAI_IMAGE_PRICE_USD
8813
- };
8814
- //#endregion
8815
- //#region src/image/openrouter.ts
8816
- /** Env var read for the OpenRouter API key when `apiKey` is not supplied in config. */
8817
- const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
8818
- const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
8819
- /** Key under which this adapter's metadata sits on `providerMetadata`. */
8820
- const METADATA_KEY = "openrouter";
8821
- /**
8822
- * The Gemini image models Motif exposes, by their bare Google names, mapped to
8823
- * the OpenRouter slug that serves them. All six are listed by
8824
- * `GET https://openrouter.ai/api/v1/images/models`.
8825
- */
8826
- const OPENROUTER_GEMINI_IMAGE_MODELS = {
8827
- "gemini-2.5-flash-image": "google/gemini-2.5-flash-image",
8828
- "gemini-3.1-flash-image-preview": "google/gemini-3.1-flash-image-preview",
8829
- "gemini-3-pro-image-preview": "google/gemini-3-pro-image-preview",
8830
- "gemini-3.1-flash-image": "google/gemini-3.1-flash-image",
8831
- "gemini-3-pro-image": "google/gemini-3-pro-image",
8832
- "gemini-3.1-flash-lite-image": "google/gemini-3.1-flash-lite-image"
8833
- };
8834
- /**
8835
- * Static USD/image estimate, keyed by the bare model name. Used only when a
8836
- * response carries no `usage.cost`; OpenRouter normally reports the real figure.
8837
- *
8838
- * - `gemini-2.5-flash-image`: 1290 output tokens at $30 / 1M ≈ $0.039.
8839
- * - `gemini-3-pro-image-preview`, `gemini-3-pro-image`: ≈ $0.134 at 1K/2K.
8840
- * - `gemini-3.1-flash-image-preview`: priced with the 2.5 flash-image line
8841
- * pending a distinct published rate.
8842
- * - `gemini-3.1-flash-image`: $0.067 at 1K.
8843
- * - `gemini-3.1-flash-lite-image`: ≈ $0.0336 at 1K.
8844
- * Source: https://ai.google.dev/gemini-api/docs/pricing
8845
- */
8846
- const OPENROUTER_IMAGE_PRICE_USD = {
8847
- "gemini-2.5-flash-image": .039,
8848
- "gemini-3.1-flash-image-preview": .039,
8849
- "gemini-3-pro-image-preview": .134,
8850
- "gemini-3.1-flash-image": .067,
8851
- "gemini-3-pro-image": .134,
8852
- "gemini-3.1-flash-lite-image": .0336
8853
- };
8854
- /**
8855
- * Resolve a Motif model name to an OpenRouter slug. A bare Gemini name maps to
8856
- * its `google/<id>` slug; a name already containing `/` is an OpenRouter slug
8857
- * and passes through. Anything else is unknown and throws.
8858
- */
8859
- function openRouterModelSlug(modelId) {
8860
- if (modelId.includes("/")) return modelId;
8861
- const slug = OPENROUTER_GEMINI_IMAGE_MODELS[modelId];
8862
- if (slug === void 0) throw new MotifError(`No OpenRouter image model for "${modelId}". Known: ${Object.keys(OPENROUTER_GEMINI_IMAGE_MODELS).join(", ")}; or pass an OpenRouter slug such as google/gemini-3.1-flash-image.`, 0);
8863
- return slug;
8864
- }
8865
- function isRecord$3(value) {
8866
- return typeof value === "object" && value !== null;
8867
- }
8868
- function toDataUrl(mediaType, data) {
8869
- return `data:${mediaType};base64,${typeof data === "string" ? data : Buffer.from(data).toString("base64")}`;
8870
- }
8871
- /** One `input_references` entry for an input file: a URL as-is, bytes as a data URL. */
8872
- function toInputReference(file) {
8873
- return {
8874
- type: "image_url",
8875
- image_url: { url: file.type === "url" ? file.url : toDataUrl(file.mediaType, file.data) }
8876
- };
8877
- }
8878
- function buildBody(slug, options) {
8879
- if (options.mask !== void 0) throw new MotifError("OpenRouter's Image API takes no mask; describe the region in the instruction instead.", 0);
8880
- return {
8881
- ...options.providerOptions[METADATA_KEY] ?? {},
8882
- model: slug,
8883
- prompt: options.prompt,
8884
- n: options.n,
8885
- ...options.aspectRatio === void 0 ? {} : { aspect_ratio: options.aspectRatio },
8886
- ...options.files === void 0 || options.files.length === 0 ? {} : { input_references: options.files.map(toInputReference) }
8887
- };
8888
- }
8889
- function errorMessage(body, fallback) {
8890
- if (isRecord$3(body) && isRecord$3(body.error)) {
8891
- const { message } = body.error;
8892
- if (typeof message === "string" && message !== "") return message;
8893
- }
8894
- return fallback;
8895
- }
8896
- function parseResponse(body) {
8897
- if (!isRecord$3(body) || !Array.isArray(body.data)) throw new MotifError("OpenRouter image response had no data array", 502);
8898
- const images = [];
8899
- for (const entry of body.data) if (isRecord$3(entry) && typeof entry.b64_json === "string") images.push(entry.b64_json);
8900
- if (images.length === 0) throw new MotifError("OpenRouter returned no images", 502);
8901
- return {
8902
- images,
8903
- cost: isRecord$3(body.usage) && typeof body.usage.cost === "number" ? body.usage.cost : void 0
8904
- };
8905
- }
8906
- function buildModel(modelId, apiKey, doFetch) {
8907
- const slug = openRouterModelSlug(modelId);
8908
- return {
8909
- specificationVersion: "v4",
8910
- provider: METADATA_KEY,
8911
- modelId: slug,
8912
- maxImagesPerCall: 10,
8913
- async doGenerate(options) {
8914
- const warnings = [];
8915
- if (options.seed !== void 0) warnings.push({
8916
- type: "unsupported",
8917
- feature: "seed"
8918
- });
8919
- if (options.size !== void 0) warnings.push({
8920
- type: "unsupported",
8921
- feature: "size",
8922
- details: "Gemini on OpenRouter takes aspectRatio, not pixel sizes."
8923
- });
8924
- const timestamp = /* @__PURE__ */ new Date();
8925
- const response = await doFetch(OPENROUTER_IMAGES_URL, {
8926
- method: "POST",
8927
- headers: {
8928
- ...options.headers,
8929
- Authorization: `Bearer ${apiKey}`,
8930
- "Content-Type": "application/json"
8931
- },
8932
- body: JSON.stringify(buildBody(slug, options)),
8933
- ...options.abortSignal === void 0 ? {} : { signal: options.abortSignal }
8934
- });
8935
- const text = await response.text();
8936
- let body;
8937
- try {
8938
- body = JSON.parse(text);
8939
- } catch {
8940
- body = void 0;
8941
- }
8942
- if (!response.ok) throw new MotifError(errorMessage(body, `OpenRouter ${response.status}: ${text}`), response.status);
8943
- const { images, cost } = parseResponse(body);
8944
- return {
8945
- images,
8946
- warnings,
8947
- providerMetadata: { [METADATA_KEY]: {
8948
- images: images.map(() => ({})),
8949
- ...cost === void 0 ? {} : { cost }
8950
- } },
8951
- response: {
8952
- timestamp,
8953
- modelId: slug,
8954
- headers: Object.fromEntries(response.headers.entries())
8955
- }
8956
- };
8957
- }
8958
- };
8959
- }
8960
- /**
8961
- * Build an OpenRouter `ImageModel`. Prefers the passed `apiKey`, else the
8962
- * `OPENROUTER_API_KEY` env var. Throws `MotifError` when neither is present or
8963
- * the model name is unknown (callers translate this into a `Result.err`).
8964
- */
8965
- function resolveModel$1(modelId, apiKey, fetch) {
8966
- const key = apiKey ?? process.env["OPENROUTER_API_KEY"];
8967
- if (key === void 0 || key === "") throw new MotifError(`OpenRouter image generation requires an API key (config.openrouter.apiKey or ${OPENROUTER_API_KEY_ENV})`, 0);
8968
- const configured = toProviderFetch(fetch);
8969
- return buildModel(modelId, key, "fetch" in configured ? configured.fetch : globalThis.fetch);
8970
- }
8971
- /** The OpenRouter provider adapter registered in the provider registry. */
8972
- const openrouterAdapter = {
8973
- id: "openrouter",
8974
- apiKeyEnv: OPENROUTER_API_KEY_ENV,
8975
- resolveModel: resolveModel$1,
8976
- priceUsdByModel: OPENROUTER_IMAGE_PRICE_USD
8977
- };
8978
- //#endregion
8979
- //#region src/image/replicate.ts
8980
- /**
8981
- * Replicate provider adapter.
8982
- *
8983
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/replicate`. Building a model
8984
- * performs no network I/O — the request only happens when `generateImage`
8985
- * invokes `model.doGenerate`.
8986
- *
8987
- * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
8988
- * and reads `REPLICATE_API_TOKEN` from the environment.
8989
- */
8990
- /** Env var read for the Replicate API token when `apiToken` is not in config. */
8991
- const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
8992
- /**
8993
- * Static Replicate USD/image, keyed by model id.
8994
- *
8995
- * `black-forest-labs/flux-1.1-pro-ultra` = $0.06/image (Replicate list price).
8996
- * Source: https://replicate.com/black-forest-labs/flux-1.1-pro-ultra
8997
- * Sanity anchor: fal's `fal-ai/flux-pro/v1.1-ultra` is also $0.06
8998
- * (`MODELS.flux.pricePerImageUsd` in ../models) — same price, both routes.
8999
- */
9000
- const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06 };
9001
- /**
9002
- * Build a Replicate `ImageModel`. Prefers the passed `apiKey` (the normalized
9003
- * token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
9004
- * neither is present (callers translate this into a `Result.err`).
9005
- */
9006
- function resolveModel(modelId, apiKey, fetch) {
9007
- const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
9008
- if (token === void 0 || token === "") throw new MotifError(`Replicate image generation requires an API token (config.replicate.apiToken or ${REPLICATE_API_KEY_ENV})`, 0);
9009
- return (0, _ai_sdk_replicate.createReplicate)({
9010
- apiToken: token,
9011
- ...toProviderFetch(fetch)
9012
- }).image(modelId);
9013
- }
9014
- //#endregion
9015
- //#region src/image/provider.ts
9016
- /**
9017
- * The provider registry. Every id in {@link ImageProviderId}'s closed part maps
9018
- * to its adapter; the open union tail means a lookup can still miss, so access
9019
- * goes through {@link getProviderAdapter}.
9020
- */
9021
- const PROVIDERS = {
9022
- openrouter: openrouterAdapter,
9023
- openai: openaiAdapter,
9024
- replicate: {
9025
- id: "replicate",
9026
- apiKeyEnv: REPLICATE_API_KEY_ENV,
9027
- resolveModel,
9028
- priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
9029
- },
9030
- fal: falAdapter
9031
- };
9032
- /**
9033
- * Look up an adapter by provider id, throwing a `MotifError` for an unknown
9034
- * provider (callers catch this into a `Result.err`).
9035
- */
9036
- function getProviderAdapter(provider) {
9037
- const adapter = PROVIDERS[provider];
9038
- if (adapter === void 0) throw new MotifError(`Unsupported image provider: ${provider}`, 0);
9039
- return adapter;
9040
- }
9041
- //#endregion
9042
- //#region src/image/cost.ts
9043
- /**
9044
- * Per-call cost tracking for the image layer.
9045
- *
9046
- * Preference order:
9047
- * 1. A cost surfaced by the provider on `result.providerMetadata` (most image
9048
- * providers do NOT surface one today, so this is usually absent).
9049
- * 2. A static per-model table, owned per-adapter (`priceUsdByModel`) and read
9050
- * from the provider registry (see sources in each adapter).
9051
- * 3. Unknown → `{ usd: 0, source: "unknown" }`.
9052
- */
9053
- function isRecord$2(value) {
9054
- return typeof value === "object" && value !== null;
9055
- }
9056
- /**
9057
- * Extract a provider-reported total cost from a `generateImage` result's
9058
- * `providerMetadata`, if the provider surfaces a numeric `cost`. The shape is
9059
- * `{ [provider]: { ...; cost?: number } }`. Returns undefined otherwise.
9060
- */
9061
- function costFromProviderMetadata(providerMetadata) {
9062
- if (!isRecord$2(providerMetadata)) return;
9063
- for (const value of Object.values(providerMetadata)) if (isRecord$2(value)) {
9064
- const cost = value.cost;
9065
- if (typeof cost === "number" && Number.isFinite(cost)) return cost;
9066
- }
9067
- }
9068
- function tablePricePerImage(provider, modelId) {
9069
- const adapter = PROVIDERS[provider];
9070
- if (adapter === void 0) return;
9071
- return adapter.priceUsdByModel[modelId];
9072
- }
9073
- function roundUsd(value) {
9074
- return Number(value.toFixed(6));
9075
- }
9076
- /**
9077
- * Cost across every underlying model call of one generation. Provider-metadata
9078
- * costs from the calls that report one are summed; otherwise the static table
9079
- * (× image count), then unknown.
9080
- */
9081
- function costForCalls(provider, modelId, callMetadata, imageCount) {
9082
- let metaCost;
9083
- for (const providerMetadata of callMetadata) {
9084
- const callCost = costFromProviderMetadata(providerMetadata);
9085
- if (callCost !== void 0) metaCost = (metaCost ?? 0) + callCost;
9086
- }
9087
- if (metaCost !== void 0) return {
9088
- usd: roundUsd(metaCost),
9089
- source: "provider-metadata"
9090
- };
9091
- const perImage = tablePricePerImage(provider, modelId);
9092
- if (perImage !== void 0) return {
9093
- usd: roundUsd(perImage * Math.max(imageCount, 1)),
9094
- source: "table"
9095
- };
9096
- return {
9097
- usd: 0,
9098
- source: "unknown"
9099
- };
9100
- }
9101
- //#endregion
9102
- //#region src/image/index.ts
9103
- /**
9104
- * `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
9105
- *
9106
- * ESM-only subpath export, built on the Vercel AI SDK image interface
9107
- * (`generateImage`, `@ai-sdk/*`). The caller names the provider and model;
9108
- * reuses the SDK's Result convention (`Result<T, MotifError>` — no
9109
- * thrown exceptions). Gemini is reached through OpenRouter.
9110
- *
9111
- * @example
9112
- * ```ts
9113
- * import { createMotifImage } from "@howells/motif-sdk/image";
9114
- *
9115
- * const img = createMotifImage({ defaultProvider: "openrouter" });
9116
- * const r = await img.generate({ model: "gemini-3.1-flash-image", prompt: "a bare concrete wall" });
9117
- * if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
9118
- * ```
9119
- */
9120
- const DEFAULT_PROVIDER = "openrouter";
9121
- /**
9122
- * Create a provider-agnostic image client.
9123
- *
9124
- * @param config - provider selection + keys (keys fall back to env).
9125
- * @param deps - INTERNAL testing seam. Defaults to the real AI SDK `generateImage`
9126
- * and the built-in provider adapters; tests inject fakes here to run offline.
9127
- */
9128
- function createMotifImage(config = {}, deps = {}) {
9129
- const generateImageFn = deps.generateImage ?? ai.generateImage;
9130
- const resolveModelFn = deps.resolveModel ?? defaultResolveModel;
9131
- function resolveProvider(provider) {
9132
- return provider ?? config.defaultProvider ?? DEFAULT_PROVIDER;
9133
- }
9134
- function apiKeyFor(provider) {
9135
- switch (provider) {
9136
- case "openrouter": return config.openrouter?.apiKey;
9137
- case "openai": return config.openai?.apiKey;
9138
- case "replicate": return config.replicate?.apiToken;
9139
- case "fal": return config.fal?.apiKey;
9140
- default: return;
9141
- }
9142
- }
9143
- async function generate(opts) {
9144
- const provider = resolveProvider(opts.provider);
9145
- try {
9146
- const modelId = opts.model;
9147
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
9148
- const result = await generateImageFn({
9149
- model,
9150
- prompt: opts.prompt,
9151
- ...opts.n === void 0 ? {} : { n: opts.n },
9152
- ...opts.size === void 0 ? {} : { size: opts.size },
9153
- ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
9154
- ...opts.seed === void 0 ? {} : { seed: opts.seed },
9155
- ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
9156
- ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
9157
- ...opts.headers === void 0 ? {} : { headers: opts.headers },
9158
- ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
9159
- });
9160
- return (0, neverthrow.ok)(toMotifImageResult(result, provider, modelId));
9161
- } catch (error) {
9162
- return (0, neverthrow.err)(toMotifError(error));
9163
- }
9164
- }
9165
- async function edit(opts) {
9166
- const provider = resolveProvider(opts.provider);
9167
- const imagesField = provider === "fal" ? falEditImagesField(opts.model) : void 0;
9168
- if (imagesField === "image_url" && opts.images.length > 1) return (0, neverthrow.err)(new MotifError(`${opts.model} takes one input image; ${opts.images.length} were given`, 0));
9169
- const providerOptions = imagesField === "image_urls" && opts.providerOptions?.fal?.useMultipleImages === void 0 ? {
9170
- ...opts.providerOptions,
9171
- fal: {
9172
- ...opts.providerOptions?.fal,
9173
- useMultipleImages: true
9174
- }
9175
- } : opts.providerOptions;
9176
- try {
9177
- const modelId = opts.model;
9178
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
9179
- const result = await generateImageFn({
9180
- model,
9181
- prompt: {
9182
- images: opts.images,
9183
- text: opts.instruction,
9184
- ...opts.mask === void 0 ? {} : { mask: opts.mask }
9185
- },
9186
- ...opts.n === void 0 ? {} : { n: opts.n },
9187
- ...opts.seed === void 0 ? {} : { seed: opts.seed },
9188
- ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
9189
- ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
9190
- ...opts.headers === void 0 ? {} : { headers: opts.headers },
9191
- ...providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(providerOptions) }
9192
- });
9193
- return (0, neverthrow.ok)(toMotifImageResult(result, provider, modelId));
9194
- } catch (error) {
9195
- return (0, neverthrow.err)(toMotifError(error));
9196
- }
9197
- }
9198
- async function bestOfN(opts) {
9199
- return await runBestOfN(opts, generate, edit);
9200
- }
9201
- return {
9202
- generate,
9203
- edit,
9204
- bestOfN
9205
- };
9206
- }
9207
- /**
9208
- * Fire one best-of-N candidate: reuse `generate`/`edit` (discriminated by the
9209
- * presence of `images`, narrowed inline so TS refines the union). Each candidate
9210
- * makes a single image (`n: 1`) — the outer `n` is the candidate count, not the
9211
- * per-call image count — and takes a distinct `seed` (`seed + index`) when a seed
9212
- * was given, so the N vary. `opts.signal` rides along via the spread, so one
9213
- * cancel aborts every candidate.
9214
- */
9215
- async function runCandidate(opts, index, generate, edit) {
9216
- const candidateSeed = opts.seed === void 0 ? void 0 : opts.seed + index;
9217
- const overrides = {
9218
- n: 1,
9219
- ...candidateSeed === void 0 ? {} : { seed: candidateSeed }
9220
- };
9221
- if ("images" in opts) return await edit({
9222
- ...opts,
9223
- ...overrides
9224
- });
9225
- return await generate({
9226
- ...opts,
9227
- ...overrides
9228
- });
9229
- }
9230
- /** Ask the judge to pick a winner and validate its index; no judge → index 0. */
9231
- async function selectWinner(opts, candidates) {
9232
- const { judge } = opts;
9233
- if (judge === void 0) return (0, neverthrow.ok)({ chosenIndex: 0 });
9234
- const decision = await judge(candidates, "images" in opts ? { instruction: opts.instruction } : { prompt: opts.prompt });
9235
- if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= candidates.length) return (0, neverthrow.err)(new MotifError(`bestOfN judge returned an out-of-range index ${decision.index} (expected 0..${candidates.length - 1})`, 0));
9236
- return (0, neverthrow.ok)({
9237
- chosenIndex: decision.index,
9238
- ...decision.reason === void 0 ? {} : { reason: decision.reason }
9239
- });
9240
- }
9241
- /**
9242
- * Best-of-N: generate `n` candidates in parallel, keep the successes, and pick a
9243
- * winner via the (optional, injectable) judge. Wraps everything so no throw
9244
- * escapes — any failure becomes a `Result.err`.
9245
- */
9246
- async function runBestOfN(opts, generate, edit) {
9247
- try {
9248
- const { n } = opts;
9249
- if (!Number.isInteger(n) || n < 1) return (0, neverthrow.err)(new MotifError(`bestOfN requires an integer n >= 1 (got ${n})`, 0));
9250
- const results = await Promise.all(Array.from({ length: n }, (_unused, index) => index).map(async (index) => await runCandidate(opts, index, generate, edit)));
9251
- const candidates = [];
9252
- let firstError;
9253
- for (const result of results) if (result.isOk()) candidates.push(result.value);
9254
- else firstError ??= result.error;
9255
- if (candidates.length === 0) return (0, neverthrow.err)(firstError ?? new MotifError(`all ${n} bestOfN candidates failed`, 0));
9256
- const winner = await selectWinner(opts, candidates);
9257
- if (winner.isErr()) return (0, neverthrow.err)(winner.error);
9258
- const { chosenIndex, reason } = winner.value;
9259
- const best = candidates[chosenIndex];
9260
- if (best === void 0) return (0, neverthrow.err)(new MotifError("bestOfN failed to resolve a winner", 0));
9261
- const totalCostUsd = Number(candidates.reduce((sum, candidate) => sum + candidate.cost.usd, 0).toFixed(6));
9262
- return (0, neverthrow.ok)({
9263
- best,
9264
- chosenIndex,
9265
- ...reason === void 0 ? {} : { reason },
9266
- candidates,
9267
- totalCostUsd
9268
- });
9269
- } catch (error) {
9270
- return (0, neverthrow.err)(toMotifError(error));
9271
- }
9272
- }
9273
- /** Default provider dispatch: resolve the model through the provider registry. */
9274
- function defaultResolveModel(provider, modelId, apiKey, fetch) {
9275
- return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
9276
- }
9277
- /** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
9278
- function toMotifImageResult(result, provider, model) {
9279
- const images = result.images.map((file) => ({
9280
- uint8Array: file.uint8Array,
9281
- base64: file.base64,
9282
- mediaType: file.mediaType
9283
- }));
9284
- const cost = costForCalls(provider, model, result.calls.map((call) => call.providerMetadata), images.length);
9285
- const requestId = extractRequestId(result);
9286
- const warnings = result.warnings.map(renderWarning);
9287
- return {
9288
- images,
9289
- cost,
9290
- provider,
9291
- model,
9292
- ...requestId === void 0 ? {} : { requestId },
9293
- ...warnings.length === 0 ? {} : { warnings }
9294
- };
9295
- }
9296
- /** Render an AI SDK `Warning` object into a readable one-line string. */
9297
- function renderWarning(warning) {
9298
- if (warning.type === "unsupported" || warning.type === "compatibility") {
9299
- const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
9300
- return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
9301
- }
9302
- if (warning.type === "deprecated") return `deprecated setting "${warning.setting}": ${warning.message}`;
9303
- return warning.message;
9304
- }
9305
- function isRecord$1(value) {
9306
- return typeof value === "object" && value !== null;
9307
- }
9308
- /** Look for a provider correlation id in providerMetadata, then response headers. */
9309
- function extractRequestId(result) {
9310
- for (const call of result.calls) {
9311
- const fromMetadata = requestIdFromMetadata(call.providerMetadata);
9312
- if (fromMetadata !== void 0) return fromMetadata;
9313
- }
9314
- for (const { response } of result.calls) {
9315
- const { headers } = response;
9316
- if (headers) {
9317
- const id = headers["x-request-id"] ?? headers["x-goog-request-id"] ?? headers["x-fal-request-id"];
9318
- if (typeof id === "string" && id !== "") return id;
9319
- }
9320
- }
9321
- }
9322
- function requestIdFromMetadata(providerMetadata) {
9323
- if (!isRecord$1(providerMetadata)) return;
9324
- for (const value of Object.values(providerMetadata)) if (isRecord$1(value)) {
9325
- const id = value.requestId ?? value.request_id;
9326
- if (typeof id === "string" && id !== "") return id;
9327
- }
9328
- }
9329
- /**
9330
- * Coerce a public `providerOptions` (values typed `unknown`) into the AI SDK's
9331
- * JSON-only `Record<string, Record<string, JSONValue>>`. Fails fast: a
9332
- * non-JSON-representable value (undefined/function/bigint/symbol) throws a
9333
- * `MotifError` naming the offending path rather than silently forwarding a value
9334
- * the caller did not intend.
9335
- */
9336
- function toProviderOptions(input) {
9337
- const out = {};
9338
- for (const [namespace, options] of Object.entries(input)) {
9339
- const inner = {};
9340
- for (const [key, value] of Object.entries(options)) inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
9341
- out[namespace] = inner;
9342
- }
9343
- return out;
9344
- }
9345
- function toJsonValue(value, path) {
9346
- if (value === null) return null;
9347
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9348
- if (typeof value === "object") {
9349
- if (Array.isArray(value)) {
9350
- const arr = [];
9351
- for (const [index, item] of value.entries()) arr.push(toJsonValue(item, `${path}[${index}]`));
9352
- return arr;
9353
- }
9354
- const obj = {};
9355
- for (const [key, entry] of Object.entries(value)) obj[key] = toJsonValue(entry, `${path}.${key}`);
9356
- return obj;
9357
- }
9358
- throw new MotifError(`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`, 0);
9359
- }
9360
- /**
9361
- * Coerce an unknown thrown value into a `MotifError`, mirroring server.ts's
9362
- * mapping: preserve an existing `MotifError`, lift a string `code`, and mark
9363
- * non-HTTP local failures with status `0`.
9364
- */
9365
- function toMotifError(error) {
9366
- if (error instanceof MotifError) return error;
9367
- const message = error instanceof Error ? error.message : String(error);
9368
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
9369
- const status = error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0;
9370
- const body = error instanceof Error && "responseBody" in error && typeof error.responseBody === "string" ? error.responseBody : message;
9371
- if (isFalAccountLocked(status, body)) return falHttpError(status, body);
9372
- return new MotifError(message, status, code);
9373
- }
9374
- //#endregion
9375
8794
  //#region src/task-output.ts
9376
8795
  function isPlainRecord(value) {
9377
8796
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -9665,14 +9084,14 @@ function projectedOutputs(model, input, body) {
9665
9084
  }
9666
9085
  function outputSize(parameters, input, imageSize) {
9667
9086
  if (typeof imageSize === "string") return FAL_PRESET_SIZES[imageSize];
9668
- if (isPixelSize$1(imageSize)) return imageSize;
9087
+ if (isPixelSize(imageSize)) return imageSize;
9669
9088
  if (imageSize === void 0) {
9670
9089
  const fallback = parameters.find((parameter) => parameter.key === "image_size")?.fallback;
9671
9090
  if (fallback === void 0) return sourceSizeOf(input);
9672
9091
  return typeof fallback === "string" ? FAL_PRESET_SIZES[fallback] : void 0;
9673
9092
  }
9674
9093
  }
9675
- function isPixelSize$1(value) {
9094
+ function isPixelSize(value) {
9676
9095
  return typeof value === "object" && value !== null && "width" in value && "height" in value && typeof value.width === "number" && typeof value.height === "number";
9677
9096
  }
9678
9097
  /** What the plan knows of the source, for prices that read it. */
@@ -10040,11 +9459,12 @@ function taskRequest(task, input) {
10040
9459
  aspect: input.aspect,
10041
9460
  count: input.count,
10042
9461
  look: task === "generate" || task === "vary" ? input.look : void 0,
9462
+ markup: input.markup !== void 0,
10043
9463
  mask: input.mask !== void 0,
10044
9464
  mode: input.mode,
10045
9465
  model: input.model,
10046
9466
  negativePrompt: input.negativePrompt !== void 0,
10047
- outputFormat: input.outputFormat !== void 0,
9467
+ outputFormat: input.outputFormat,
10048
9468
  references,
10049
9469
  resolution: (task === "generate" || task === "vary") && input.resolution === "2K" ? void 0 : input.resolution,
10050
9470
  rig: input.rig,
@@ -10057,7 +9477,6 @@ function taskRequest(task, input) {
10057
9477
  function resolve(task, input, context, options) {
10058
9478
  const keys = [];
10059
9479
  if (options.dryRun === true || context.falKey !== void 0) keys.push("FAL_KEY");
10060
- if (options.dryRun === true || context.openAiKey !== void 0) keys.push("OPENAI_API_KEY");
10061
9480
  const resolution = resolveTask(task, taskRequest(task, input), {
10062
9481
  keys,
10063
9482
  pins: context.pins
@@ -10132,7 +9551,7 @@ function withCode(error, model, task) {
10132
9551
  field: error.field,
10133
9552
  task
10134
9553
  });
10135
- const motif = toMotifError$1(error);
9554
+ const motif = toMotifError(error);
10136
9555
  if (motif.code !== void 0) return motif;
10137
9556
  return new MotifError(motif.message, 0, INVALID_OPTION, void 0, {
10138
9557
  model,
@@ -10155,6 +9574,7 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10155
9574
  "aspect",
10156
9575
  "count",
10157
9576
  "look",
9577
+ "markup",
10158
9578
  "mask",
10159
9579
  "mood",
10160
9580
  "negativePrompt",
@@ -10165,11 +9585,23 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10165
9585
  "seed",
10166
9586
  "transparent"
10167
9587
  ]);
9588
+ const IMAGE_QUALITIES = [
9589
+ "auto",
9590
+ "low",
9591
+ "medium",
9592
+ "high",
9593
+ "xhigh",
9594
+ "max"
9595
+ ];
9596
+ function isImageQuality(value) {
9597
+ return IMAGE_QUALITIES.some((level) => level === value);
9598
+ }
10168
9599
  /** The TaskInput field behind each option `buildGenerateBody` can refuse. */
10169
9600
  const FIELD_FOR_OPTION = {
10170
9601
  aspect: "aspect",
10171
9602
  background: "transparent",
10172
9603
  "image editing": "references",
9604
+ markupImageUrl: "markup",
10173
9605
  maskImageUrl: "mask",
10174
9606
  negativePrompt: "negativePrompt",
10175
9607
  numImages: "count",
@@ -10209,23 +9641,34 @@ function generationPlan(task, model, config, input) {
10209
9641
  task
10210
9642
  }));
10211
9643
  const editImageUrls = [...task === "vary" && input.image !== void 0 ? [input.image] : [], ...input.references ?? []];
9644
+ if (input.markup !== void 0 && editImageUrls.length === 0) return (0, neverthrow.err)(invalidOption("Markup applies to the image being edited; pass references.", {
9645
+ field: "markup",
9646
+ requires: "references"
9647
+ }));
10212
9648
  if (input.mask !== void 0 && editImageUrls.length === 0) return (0, neverthrow.err)(invalidOption("A mask applies to a reference image; pass references.", {
10213
9649
  field: "mask",
10214
9650
  requires: "references"
10215
9651
  }));
10216
- if (input.transparent === true && config.supportsBackground !== true && config.transparencyRoute !== void 0) return openAiPlan(task, model, config.transparencyRoute, input, editImageUrls);
10217
- if (input.outputFormat !== void 0 && config.supportedOutputFormats !== void 0 && !config.supportedOutputFormats.includes(input.outputFormat)) return (0, neverthrow.err)(cannotCarry("outputFormat", model, task));
9652
+ if (input.outputFormat !== void 0 && !outputFormatsOf(config, editImageUrls.length > 0).includes(input.outputFormat)) return (0, neverthrow.err)(cannotCarry("outputFormat", model, task));
9653
+ const { quality, ...params } = input.params ?? {};
9654
+ const liftQuality = config.supportsQuality === true && quality !== void 0;
9655
+ if (liftQuality && !isImageQuality(quality)) return (0, neverthrow.err)(invalidOption(`params.quality must be one of ${IMAGE_QUALITIES.join(", ")}.`, {
9656
+ field: "params",
9657
+ key: "quality"
9658
+ }));
10218
9659
  const options = {
10219
9660
  aspect: input.aspect,
10220
9661
  background: input.transparent === true ? "transparent" : void 0,
10221
9662
  creative: creativeOf(input),
10222
9663
  editImageUrls: editImageUrls.length > 0 ? editImageUrls : void 0,
9664
+ markupImageUrl: input.markup,
10223
9665
  maskImageUrl: input.mask,
10224
9666
  model,
10225
9667
  negativePrompt: input.negativePrompt,
10226
9668
  numImages: input.count,
10227
9669
  outputFormat: input.outputFormat,
10228
9670
  prompt: task === "vary" && (input.prompt ?? "") === "" ? VARY_PROMPT : input.prompt ?? "",
9671
+ quality: liftQuality && isImageQuality(quality) ? quality : void 0,
10229
9672
  resolution: input.resolution,
10230
9673
  seed: input.seed
10231
9674
  };
@@ -10235,7 +9678,7 @@ function generationPlan(task, model, config, input) {
10235
9678
  } catch (error) {
10236
9679
  return (0, neverthrow.err)(generationError(error, model, task));
10237
9680
  }
10238
- const merged = mergeParams(built.body, input.params, ownedGenerationKeys(input));
9681
+ const merged = mergeParams(built.body, liftQuality ? params : input.params, ownedGenerationKeys(input));
10239
9682
  if (merged.isErr()) return (0, neverthrow.err)(merged.error);
10240
9683
  const badSize = checkParamsImageSize(config, model, task, input.params);
10241
9684
  if (badSize !== void 0) return (0, neverthrow.err)(badSize);
@@ -10248,55 +9691,6 @@ function generationPlan(task, model, config, input) {
10248
9691
  queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
10249
9692
  });
10250
9693
  }
10251
- /**
10252
- * The OpenAI transparency route. Ported from the CLI's generate-openai: the
10253
- * route carries only what gpt-image takes, so everything else is refused.
10254
- */
10255
- function openAiPlan(task, model, route, input, editImageUrls) {
10256
- const allowed = /* @__PURE__ */ new Set([
10257
- "aspect",
10258
- "count",
10259
- "look",
10260
- "mask",
10261
- "mood",
10262
- "prompt",
10263
- "references",
10264
- "transparent"
10265
- ]);
10266
- if (task === "vary") allowed.add("image");
10267
- if (input.outputFormat === "png") allowed.add("outputFormat");
10268
- const refused = refuseOthers(input, allowed, model, task);
10269
- if (refused !== void 0) return (0, neverthrow.err)(refused);
10270
- if (input.params !== void 0) return (0, neverthrow.err)(cannotCarry("params", model, task));
10271
- const editing = editImageUrls.length > 0;
10272
- if (editing && !route.supportsEdit) return (0, neverthrow.err)(cannotCarry("references", model, task));
10273
- let prompt;
10274
- try {
10275
- const creative = creativeOf(input);
10276
- prompt = creative === void 0 ? input.prompt ?? "" : enrichPrompt({
10277
- creative,
10278
- prompt: input.prompt ?? ""
10279
- }).prompt;
10280
- } catch (error) {
10281
- return (0, neverthrow.err)(withCode(error, model, task));
10282
- }
10283
- return (0, neverthrow.ok)({
10284
- body: {
10285
- n: input.count ?? 1,
10286
- prompt,
10287
- size: aspectToGptSize(input.aspect ?? "1:1"),
10288
- ...editing && { images: [...editImageUrls] },
10289
- ...input.mask !== void 0 && { mask: input.mask },
10290
- background: "transparent",
10291
- outputFormat: "png"
10292
- },
10293
- cost: UNKNOWN_COST,
10294
- endpoint: `${route.provider}:${route.model}`,
10295
- prompt,
10296
- provider: "openai",
10297
- queued: false
10298
- });
10299
- }
10300
9694
  function flatPrice(pricing) {
10301
9695
  const match = FLAT_PRICE_REGEX.exec(pricing.trim());
10302
9696
  return match === null ? null : Number(match[1]);
@@ -10348,7 +9742,7 @@ function parsePayload(text) {
10348
9742
  if (JSON_START.test(text.trim())) throw error;
10349
9743
  raw = text;
10350
9744
  }
10351
- if (!isRecord$4(raw) || Array.isArray(raw)) return {
9745
+ if (!isRecord$1(raw) || Array.isArray(raw)) return {
10352
9746
  raw,
10353
9747
  files: []
10354
9748
  };
@@ -10621,8 +10015,8 @@ function enforceBufferLimit(length, requestId) {
10621
10015
  /**
10622
10016
  * The Task client: one function per Task. `createMotif()` returns a client
10623
10017
  * whose `generate`, `erase`, `upscale` and the rest resolve the Model through
10624
- * `resolveTask`, build the request that Model needs, run it on fal (or OpenAI
10625
- * for a transparency route) and return the files and fields it produced.
10018
+ * `resolveTask`, build the request that Model needs, run it on fal and return
10019
+ * the files and fields it produced.
10626
10020
  *
10627
10021
  * @example
10628
10022
  * ```ts
@@ -10632,7 +10026,6 @@ function enforceBufferLimit(length, requestId) {
10632
10026
  * ```
10633
10027
  */
10634
10028
  const MISSING_API_KEY = "MISSING_API_KEY";
10635
- const PIXEL_SIZE_REGEX = /^\d+x\d+$/;
10636
10029
  function presentKey(value) {
10637
10030
  return value === void 0 || value === "" ? void 0 : value;
10638
10031
  }
@@ -10650,9 +10043,6 @@ function outputKeysFor(model, body) {
10650
10043
  if (model === "clarity" || model === "crystal") return ["image"];
10651
10044
  return ["images"];
10652
10045
  }
10653
- function isPixelSize(size) {
10654
- return PIXEL_SIZE_REGEX.test(size);
10655
- }
10656
10046
  function isRecord(value) {
10657
10047
  return typeof value === "object" && value !== null;
10658
10048
  }
@@ -10703,106 +10093,6 @@ function falOutput(plan, result) {
10703
10093
  tier: plan.tier
10704
10094
  };
10705
10095
  }
10706
- const DEFAULT_TIMEOUT_MS = 12e4;
10707
- /**
10708
- * Remote images as bytes, fetched through the configured seam: the AI SDK
10709
- * would otherwise download them with global fetch. Data URLs pass through.
10710
- */
10711
- async function localImages(sources, fetch) {
10712
- const images = [];
10713
- for (const source of sources) {
10714
- if (!source.startsWith("https://") && !source.startsWith("http://")) {
10715
- images.push(source);
10716
- continue;
10717
- }
10718
- try {
10719
- const response = await fetch(source, { method: "GET" });
10720
- if (!response.ok) return (0, neverthrow.err)(new MotifError(`Could not download ${source}: ${response.status}`, response.status, "DOWNLOAD_FAILED"));
10721
- images.push(new Uint8Array(await response.arrayBuffer()));
10722
- } catch (error) {
10723
- return (0, neverthrow.err)(new MotifError(`Could not download ${source}: ${error instanceof Error ? error.message : String(error)}`, 0, "DOWNLOAD_FAILED"));
10724
- }
10725
- }
10726
- return (0, neverthrow.ok)(images);
10727
- }
10728
- async function openAiOutput(plan, openAiKey, config) {
10729
- const fetch = config.fetch ?? (async (url, init) => await globalThis.fetch(url, init));
10730
- const client = createMotifImage({
10731
- defaultProvider: "openai",
10732
- fetch,
10733
- openai: { apiKey: openAiKey },
10734
- ...config.retries !== void 0 && { maxRetries: config.retries }
10735
- });
10736
- const signal = AbortSignal.timeout(config.timeout ?? DEFAULT_TIMEOUT_MS);
10737
- const { body } = plan;
10738
- const [provider, model = ""] = plan.endpoint.split(":");
10739
- const n = typeof body.n === "number" ? body.n : 1;
10740
- const prompt = typeof body.prompt === "string" ? body.prompt : "";
10741
- const providerOptions = { openai: {
10742
- background: "transparent",
10743
- outputFormat: "png"
10744
- } };
10745
- const sources = Array.isArray(body.images) ? body.images.filter((item) => typeof item === "string") : [];
10746
- const mask = typeof body.mask === "string" ? [body.mask] : [];
10747
- const downloaded = await localImages([...sources, ...mask], fetch);
10748
- if (downloaded.isErr()) return (0, neverthrow.err)(downloaded.error);
10749
- const images = downloaded.value.slice(0, sources.length);
10750
- const maskImage = downloaded.value[sources.length];
10751
- const size = typeof body.size === "string" ? body.size : "auto";
10752
- const common = {
10753
- model,
10754
- n,
10755
- provider,
10756
- providerOptions,
10757
- signal
10758
- };
10759
- const result = images.length === 0 ? await client.generate({
10760
- ...common,
10761
- prompt,
10762
- ...isPixelSize(size) && { size }
10763
- }) : await client.edit({
10764
- ...common,
10765
- images,
10766
- instruction: prompt,
10767
- ...maskImage !== void 0 && { mask: maskImage }
10768
- });
10769
- if (result.isErr()) return (0, neverthrow.err)(result.error);
10770
- const value = result.value;
10771
- let cost = {
10772
- basis: "unknown",
10773
- usd: null
10774
- };
10775
- if (value.cost.source === "provider-metadata") cost = {
10776
- basis: "measured",
10777
- usd: value.cost.usd
10778
- };
10779
- else if (value.cost.source === "table") cost = {
10780
- basis: "projected",
10781
- usd: value.cost.usd
10782
- };
10783
- return (0, neverthrow.ok)({
10784
- chosenBy: plan.chosenBy,
10785
- cost,
10786
- data: {},
10787
- endpoint: plan.endpoint,
10788
- files: value.images.map((image) => ({
10789
- key: "images",
10790
- url: `data:${image.mediaType};base64,${image.base64}`
10791
- })),
10792
- model: plan.model,
10793
- ...plan.mode !== void 0 && { mode: plan.mode },
10794
- ...plan.prompt !== void 0 && { prompt: plan.prompt },
10795
- provider: plan.provider,
10796
- raw: {
10797
- model: value.model,
10798
- provider: value.provider,
10799
- ...value.warnings !== void 0 && { warnings: value.warnings }
10800
- },
10801
- ...value.requestId !== void 0 && { requestId: value.requestId },
10802
- task: plan.task,
10803
- tier: plan.tier
10804
- });
10805
- }
10806
10096
  function taskStreamer(config, plan, context) {
10807
10097
  return async (task, input, options = {}) => {
10808
10098
  const planned = plan(task, input);
@@ -10813,12 +10103,11 @@ function taskStreamer(config, plan, context) {
10813
10103
  });
10814
10104
  };
10815
10105
  }
10816
- /** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
10106
+ /** Create the Task client. The key falls back to FAL_KEY. */
10817
10107
  function createMotif(config = {}) {
10818
10108
  function context() {
10819
10109
  return {
10820
10110
  falKey: presentKey(config.falKey ?? getFalKeyFromEnv()),
10821
- openAiKey: presentKey(config.openAiKey ?? getOpenAiKeyFromEnv()),
10822
10111
  pins: config.pins
10823
10112
  };
10824
10113
  }
@@ -10841,11 +10130,6 @@ function createMotif(config = {}) {
10841
10130
  const planned = plan(task, input);
10842
10131
  if (planned.isErr()) return (0, neverthrow.err)(planned.error);
10843
10132
  const chosen = planned.value;
10844
- if (chosen.provider === "openai") {
10845
- const openAiKey = context().openAiKey;
10846
- if (openAiKey === void 0) return (0, neverthrow.err)(new MotifError("OPENAI_API_KEY is not set.", 0, MISSING_API_KEY, void 0, { envVar: "OPENAI_API_KEY" }));
10847
- return await openAiOutput(chosen, openAiKey, config);
10848
- }
10849
10133
  const headers = input.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
10850
10134
  const prepared = {
10851
10135
  body: chosen.body,
@@ -10853,13 +10137,12 @@ function createMotif(config = {}) {
10853
10137
  headers
10854
10138
  };
10855
10139
  const executor = falRequestExecutor(client.value);
10856
- return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
10140
+ return (chosen.queued || input.onSubmitted !== void 0 ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
10857
10141
  }
10858
10142
  async function resume(task, input, requestId) {
10859
10143
  const planned = plan(task, input);
10860
10144
  if (planned.isErr()) return (0, neverthrow.err)(planned.error);
10861
10145
  const chosen = planned.value;
10862
- if (chosen.provider !== "fal") return (0, neverthrow.err)(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
10863
10146
  const client = falClient();
10864
10147
  if (client.isErr()) return (0, neverthrow.err)(client.error);
10865
10148
  return (await awaitQueuedRequest(falRequestExecutor(client.value), {
@@ -10932,7 +10215,6 @@ Object.defineProperty(exports, "err", {
10932
10215
  exports.formatCost = formatCost;
10933
10216
  exports.getFalKeyFromEnv = getFalKeyFromEnv;
10934
10217
  exports.getLook = getLook;
10935
- exports.getOpenAiKeyFromEnv = getOpenAiKeyFromEnv;
10936
10218
  exports.isFalAccountLocked = isFalAccountLocked;
10937
10219
  exports.isTaskId = isTaskId;
10938
10220
  exports.modelProfile = modelProfile;