@howells/motif-sdk 5.4.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.js CHANGED
@@ -1,10 +1,6 @@
1
1
  import { err, err as err$1, ok, ok as ok$1 } from "neverthrow";
2
2
  import { defineEnv } from "@howells/envy";
3
3
  import { z } from "zod";
4
- import { generateImage } from "ai";
5
- import { createFal } from "@ai-sdk/fal";
6
- import { createOpenAI } from "@ai-sdk/openai";
7
- import { createReplicate } from "@ai-sdk/replicate";
8
4
  //#region src/aspects.ts
9
5
  /** Ordered by popularity: square first, then common ratios */
10
6
  const ASPECT_RATIOS = [
@@ -354,7 +350,7 @@ const CREATIVE_TAXONOMY = {
354
350
  experimental: true,
355
351
  id: "illustration",
356
352
  label: "Editorial illustration",
357
- model: "gpt2"
353
+ model: "flare"
358
354
  }
359
355
  ],
360
356
  mood: [
@@ -537,7 +533,7 @@ function falHttpError(status, body, requestId) {
537
533
  * `CreativeOptionError`'s `"INVALID_OPTION"`) onto the returned error so callers
538
534
  * keep structured error metadata. Status `0` marks a non-HTTP local error.
539
535
  */
540
- function toMotifError$1(error) {
536
+ function toMotifError(error) {
541
537
  if (error instanceof MotifError) return error;
542
538
  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);
543
539
  }
@@ -691,20 +687,13 @@ function promptWarnings(prompt, context = {}) {
691
687
  }
692
688
  //#endregion
693
689
  //#region src/env.ts
694
- const motifEnvSchema = defineEnv({ optional: {
695
- FAL_KEY: z.string().min(1),
696
- OPENAI_API_KEY: z.string().min(1)
697
- } });
690
+ const motifEnvSchema = defineEnv({ optional: { FAL_KEY: z.string().min(1) } });
698
691
  function parseMotifEnv(input = process.env) {
699
692
  return motifEnvSchema.parse(input);
700
693
  }
701
694
  function getFalKeyFromEnv(input = process.env) {
702
695
  return parseMotifEnv(input).FAL_KEY;
703
696
  }
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
697
  //#endregion
709
698
  //#region src/models.ts
710
699
  const AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-10-07";
@@ -849,77 +838,6 @@ const MODELS = {
849
838
  type: "generation",
850
839
  useQueue: true
851
840
  },
852
- gpt2: {
853
- benchmark: {
854
- artificialAnalysis: {
855
- editing: {
856
- elo: 1125,
857
- rank: 5
858
- },
859
- snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
860
- sourceUrls: AA_IMAGE_SOURCES,
861
- textToImage: {
862
- elo: 1172,
863
- rank: 3
864
- }
865
- },
866
- speed: {
867
- medianSeconds: 200.7,
868
- p95Seconds: 232.6,
869
- source: "artificial-analysis-models"
870
- },
871
- tiers: {
872
- price: "ultra",
873
- quality: "frontier",
874
- speed: "very_slow"
875
- },
876
- useCase: "Established frontier generation and transparent PNGs"
877
- },
878
- customImageSize: {
879
- maxEdge: 3840,
880
- maxPixels: 8294400,
881
- maxRatio: 3,
882
- minPixels: 655360,
883
- multipleOf: 16
884
- },
885
- editEndpoint: "openai/gpt-image-2/edit",
886
- endpoint: "openai/gpt-image-2",
887
- falPricing: {
888
- checkedAt: FAL_PRICING_CHECKED_AT,
889
- currency: "USD",
890
- endpointId: "openai/gpt-image-2",
891
- estimatedCostPerImageUsd: .211,
892
- source: "fal-pricing-api",
893
- unit: "units",
894
- unitPrice: 1
895
- },
896
- maxReferenceImages: 16,
897
- name: "GPT Image 2",
898
- pricePerImageUsd: .211,
899
- pricing: "$0.211",
900
- sizeMode: "image_size_enum",
901
- supportsAspect: true,
902
- streaming: {
903
- generation: true,
904
- edit: true
905
- },
906
- supportsEdit: true,
907
- supportsMaskImage: true,
908
- maskImageField: "mask_url",
909
- supportsNumImages: true,
910
- supportsOutputFormat: true,
911
- supportsQuality: true,
912
- supportsResolution: false,
913
- supportsSyncMode: true,
914
- transparencyRoute: {
915
- apiKeyEnv: "OPENAI_API_KEY",
916
- model: "gpt-image-2",
917
- provider: "openai",
918
- supportsEdit: true
919
- },
920
- type: "generation",
921
- useQueue: true
922
- },
923
841
  gpt: {
924
842
  benchmark: {
925
843
  artificialAnalysis: {
@@ -2501,7 +2419,6 @@ const MODELS = {
2501
2419
  const GENERATION_MODELS = [
2502
2420
  "sunburst",
2503
2421
  "flare",
2504
- "gpt2",
2505
2422
  "gpt",
2506
2423
  "banana21",
2507
2424
  "banana2",
@@ -2566,7 +2483,6 @@ function estimateCost(model, resolution, numImages = 1, editImages = 0) {
2566
2483
  return configuredPrice * numImages;
2567
2484
  }
2568
2485
  switch (model) {
2569
- case "gpt2": return .211 * numImages;
2570
2486
  case "gpt": return .133 * numImages;
2571
2487
  case "banana":
2572
2488
  case "gemini3": return (resolution === "4K" ? .3 : .15) * numImages;
@@ -2595,7 +2511,7 @@ function estimateVideoCost(durationSeconds = 5, generateAudio = true, model = "k
2595
2511
  * `Response.json()` yields `any`; these helpers narrow that untyped payload into
2596
2512
  * the SDK's response types with runtime guards instead of unchecked assertions.
2597
2513
  */
2598
- function isRecord$4(value) {
2514
+ function isRecord$1(value) {
2599
2515
  return typeof value === "object" && value !== null;
2600
2516
  }
2601
2517
  function asString(value) {
@@ -2605,7 +2521,7 @@ function asNumber(value) {
2605
2521
  return typeof value === "number" ? value : void 0;
2606
2522
  }
2607
2523
  function toMotifImage(value) {
2608
- if (isRecord$4(value)) return {
2524
+ if (isRecord$1(value)) return {
2609
2525
  content_type: asString(value.content_type),
2610
2526
  height: asNumber(value.height),
2611
2527
  url: asString(value.url) ?? "",
@@ -2620,7 +2536,7 @@ function parseImages(value) {
2620
2536
  function parseLogs(value) {
2621
2537
  if (!Array.isArray(value)) return;
2622
2538
  const entries = [];
2623
- for (const entry of value) if (isRecord$4(entry)) entries.push({
2539
+ for (const entry of value) if (isRecord$1(entry)) entries.push({
2624
2540
  message: asString(entry.message) ?? "",
2625
2541
  timestamp: asString(entry.timestamp) ?? ""
2626
2542
  });
@@ -2650,7 +2566,7 @@ function queueAppPath(endpoint) {
2650
2566
  return endpoint.split("/").slice(0, 2).join("/");
2651
2567
  }
2652
2568
  function parseQueueSubmission(data) {
2653
- if (!isRecord$4(data)) return { requestId: "" };
2569
+ if (!isRecord$1(data)) return { requestId: "" };
2654
2570
  return {
2655
2571
  requestId: asString(data.request_id) ?? "",
2656
2572
  responseUrl: asString(data.response_url)
@@ -2690,7 +2606,7 @@ function requestIdFromBody(text) {
2690
2606
  } catch {
2691
2607
  return;
2692
2608
  }
2693
- if (!isRecord$4(parsed)) return;
2609
+ if (!isRecord$1(parsed)) return;
2694
2610
  return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
2695
2611
  }
2696
2612
  //#endregion
@@ -2780,8 +2696,7 @@ var UnsupportedOptionError = class extends Error {
2780
2696
  constructor(config, option) {
2781
2697
  const models = modelsSupporting(option);
2782
2698
  const supported = supportedOptions(config);
2783
- 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}).` : "";
2784
- 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}`);
2699
+ 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"}.`);
2785
2700
  this.name = "UnsupportedOptionError";
2786
2701
  this.model = config.name;
2787
2702
  this.modelsSupporting = models;
@@ -2902,7 +2817,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2902
2817
  * Models use different sizing APIs:
2903
2818
  * - `gpt_size`: GPT's fixed dimensions (1024x1024, 1536x1024, etc.)
2904
2819
  * - `aspect_ratio`: string like "16:9" (Gemini, Grok, FLUX Pro Ultra)
2905
- * - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image 2)
2820
+ * - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image)
2906
2821
  * - `none`: no size control (video models)
2907
2822
  */
2908
2823
  function buildGenerateBody(options) {
@@ -4556,7 +4471,7 @@ async function runRequest(exec, prepared) {
4556
4471
  const response = await exec.request(`${FAL_BASE_URL$1}/${prepared.endpoint}`, requestInit(prepared));
4557
4472
  if (response.isErr()) return err$1(response.error);
4558
4473
  const data = await response.value.json();
4559
- const record = isRecord$4(data) ? data : {};
4474
+ const record = isRecord$1(data) ? data : {};
4560
4475
  const requestId = response.value.headers.get("x-fal-request-id") ?? asString(record.request_id);
4561
4476
  return ok$1({
4562
4477
  data: record,
@@ -4583,7 +4498,7 @@ async function getToolResult(exec, job) {
4583
4498
  const response = await exec.request(url);
4584
4499
  if (response.isErr()) return err$1(response.error);
4585
4500
  const data = await response.value.json();
4586
- return ok$1(isRecord$4(data) ? data : {});
4501
+ return ok$1(isRecord$1(data) ? data : {});
4587
4502
  }
4588
4503
  /** Poll a submitted job to completion and fetch its result. Submits nothing. */
4589
4504
  async function awaitQueuedRequest(exec, job, onProgress) {
@@ -4692,7 +4607,7 @@ var FalClient = class {
4692
4607
  try {
4693
4608
  built = buildGenerateBody(options);
4694
4609
  } catch (error) {
4695
- return err$1(toMotifError$1(error));
4610
+ return err$1(toMotifError(error));
4696
4611
  }
4697
4612
  const { endpoint, body } = built;
4698
4613
  const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
@@ -4730,7 +4645,7 @@ var FalClient = class {
4730
4645
  try {
4731
4646
  built = buildGenerateBody(options);
4732
4647
  } catch (error) {
4733
- return err$1(toMotifError$1(error));
4648
+ return err$1(toMotifError(error));
4734
4649
  }
4735
4650
  const { endpoint, body } = built;
4736
4651
  const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
@@ -4752,7 +4667,7 @@ var FalClient = class {
4752
4667
  const response = await this.request(url);
4753
4668
  if (response.isErr()) return err$1(response.error);
4754
4669
  const data = await response.value.json();
4755
- const record = isRecord$4(data) ? data : {};
4670
+ const record = isRecord$1(data) ? data : {};
4756
4671
  const rawStatus = asString(record.status);
4757
4672
  let status;
4758
4673
  if (rawStatus === "IN_QUEUE") status = "queued";
@@ -4860,7 +4775,7 @@ var FalClient = class {
4860
4775
  const response = await this.request(url);
4861
4776
  if (response.isErr()) return err$1(response.error);
4862
4777
  const data = await response.value.json();
4863
- const video = isRecord$4(data) && isRecord$4(data.video) ? data.video : void 0;
4778
+ const video = isRecord$1(data) && isRecord$1(data.video) ? data.video : void 0;
4864
4779
  if (video === void 0) return err$1(new MotifError("No video in response", 0));
4865
4780
  return ok$1({
4866
4781
  contentType: asString(video.content_type) ?? "",
@@ -4884,8 +4799,8 @@ var FalClient = class {
4884
4799
  });
4885
4800
  if (initiateResponse.isErr()) return err$1(initiateResponse.error);
4886
4801
  const initiateData = await initiateResponse.value.json();
4887
- const fileUrl = isRecord$4(initiateData) ? asString(initiateData.file_url) ?? "" : "";
4888
- const uploadUrl = isRecord$4(initiateData) ? asString(initiateData.upload_url) : void 0;
4802
+ const fileUrl = isRecord$1(initiateData) ? asString(initiateData.file_url) ?? "" : "";
4803
+ const uploadUrl = isRecord$1(initiateData) ? asString(initiateData.upload_url) : void 0;
4889
4804
  if (uploadUrl === void 0 || uploadUrl === "") return err$1(new MotifError("Upload initiate response missing upload_url", 0));
4890
4805
  let putResponse;
4891
4806
  try {
@@ -5037,7 +4952,7 @@ var FalClient = class {
5037
4952
  * Some APIs return `{ image: {...} }` instead of `{ images: [...] }`.
5038
4953
  */
5039
4954
  normalizeResponse(data, fallbackRequestId) {
5040
- if (!isRecord$4(data)) return err$1(new MotifError("Unexpected fal response shape", 0, "FAL_ERROR"));
4955
+ if (!isRecord$1(data)) return err$1(new MotifError("Unexpected fal response shape", 0, "FAL_ERROR"));
5041
4956
  const obj = data;
5042
4957
  const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
5043
4958
  if ("detail" in obj) return err$1(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
@@ -5359,13 +5274,6 @@ const BOARDS = {
5359
5274
  preliminary: true,
5360
5275
  votes: 39218
5361
5276
  },
5362
- gpt2: {
5363
- elo: 1381,
5364
- rank: 3,
5365
- uncertainty: 4,
5366
- evaluatedModel: "GPT-Image-2 (Medium)",
5367
- votes: 103385
5368
- },
5369
5277
  banana21: {
5370
5278
  elo: 1325,
5371
5279
  rank: 5,
@@ -5461,12 +5369,6 @@ const BOARDS = {
5461
5369
  evaluatedModel: "GPT-Image-2.5 Flare",
5462
5370
  preliminary: true
5463
5371
  },
5464
- gpt2: {
5465
- elo: 1460,
5466
- rank: 4,
5467
- uncertainty: 3,
5468
- evaluatedModel: "GPT-Image-2 (Medium)"
5469
- },
5470
5372
  "grok-image-2": {
5471
5373
  elo: 1439,
5472
5374
  rank: 5,
@@ -5577,10 +5479,6 @@ const GENERATION_MODELS_RANKED = withImageBenchmarks([
5577
5479
  model: "flare",
5578
5480
  tier: "quality"
5579
5481
  },
5580
- {
5581
- model: "gpt2",
5582
- tier: "quality"
5583
- },
5584
5482
  {
5585
5483
  model: "seedream5",
5586
5484
  tier: "quality"
@@ -5677,10 +5575,6 @@ const EDITING_MODELS_RANKED = withImageBenchmarks([
5677
5575
  model: "flare",
5678
5576
  tier: "quality"
5679
5577
  },
5680
- {
5681
- model: "gpt2",
5682
- tier: "quality"
5683
- },
5684
5578
  {
5685
5579
  model: "seedream5",
5686
5580
  tier: "quality"
@@ -8603,9 +8497,7 @@ const NO_MODEL_AVAILABLE = "NO_MODEL_AVAILABLE";
8603
8497
  const FAL_KEY = "FAL_KEY";
8604
8498
  function generationProfile(config) {
8605
8499
  const capabilities = /* @__PURE__ */ new Set(["image"]);
8606
- const keyed = /* @__PURE__ */ new Map();
8607
8500
  if (config.supportsBackground === true || config.transparentOutput === true) capabilities.add("transparency");
8608
- else if (config.transparencyRoute !== void 0) keyed.set("transparency", config.transparencyRoute.apiKeyEnv);
8609
8501
  if (config.supportsEdit) capabilities.add("references");
8610
8502
  if (config.supportsMaskImage === true) capabilities.add("mask");
8611
8503
  if (config.supportsMarkupImage === true) capabilities.add("markup");
@@ -8618,7 +8510,6 @@ function generationProfile(config) {
8618
8510
  return {
8619
8511
  aspects: config.supportedAspects,
8620
8512
  capabilities,
8621
- keyedCapabilities: keyed,
8622
8513
  keys: [FAL_KEY],
8623
8514
  maxReferences: config.supportsEdit ? config.maxReferenceImages ?? 1 : 0,
8624
8515
  outputFormats: {
@@ -8651,7 +8542,6 @@ function toolProfile(tool) {
8651
8542
  if (takesReference) capabilities.add("references");
8652
8543
  return {
8653
8544
  capabilities,
8654
- keyedCapabilities: /* @__PURE__ */ new Map(),
8655
8545
  keys: [FAL_KEY],
8656
8546
  maxReferences: takesReference ? 1 : 0
8657
8547
  };
@@ -8707,25 +8597,10 @@ function check(profile, needed, request, keys, requires = []) {
8707
8597
  missingInput: true,
8708
8598
  ok: false
8709
8599
  };
8710
- for (const capability of needed) {
8711
- if (profile.capabilities.has(capability)) {
8712
- if (!withinLimits(profile, capability, request)) return {
8713
- blockedBy: capability,
8714
- ok: false
8715
- };
8716
- continue;
8717
- }
8718
- const key = profile.keyedCapabilities.get(capability);
8719
- if (key === void 0) return {
8720
- blockedBy: capability,
8721
- ok: false
8722
- };
8723
- if (!keys.includes(key)) return {
8724
- blockedBy: "key",
8725
- missingKey: key,
8726
- ok: false
8727
- };
8728
- }
8600
+ for (const capability of needed) if (!profile.capabilities.has(capability) || !withinLimits(profile, capability, request)) return {
8601
+ blockedBy: capability,
8602
+ ok: false
8603
+ };
8729
8604
  return { ok: true };
8730
8605
  }
8731
8606
  const TIER_ORDER = {
@@ -8915,730 +8790,6 @@ function tierChangesChoice(task, request, environment) {
8915
8790
  return chosen.size > 1;
8916
8791
  }
8917
8792
  //#endregion
8918
- //#region src/image/fetch.ts
8919
- function urlOf$1(input) {
8920
- if (typeof input === "string") return input;
8921
- return input instanceof URL ? input.href : input.url;
8922
- }
8923
- /** Adapt a configured fetch for a provider factory; undefined keeps global fetch. */
8924
- function toProviderFetch(fetch) {
8925
- if (fetch === void 0) return {};
8926
- return { fetch: async (input, init) => await fetch(urlOf$1(input), init ?? {}) };
8927
- }
8928
- //#endregion
8929
- //#region src/image/fal.ts
8930
- /**
8931
- * fal provider adapter.
8932
- *
8933
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/fal`. Building a model
8934
- * performs no network I/O — the request only happens when `generateImage`
8935
- * invokes `model.doGenerate`. This is the lightweight fal *image* adapter; the
8936
- * richer `FalClient` fal client (queue, upload, upscale, rmbg, video, tools)
8937
- * is a separate, later fold (design doc §8, phase 1d).
8938
- *
8939
- * ENDPOINT QUIRK (Phase 0): fal's gpt-image endpoint wants `image_size` as a
8940
- * STRING enum (e.g. "1024x1024"), passed at generate time via
8941
- * `providerOptions.fal.image_size` — NOT the AI SDK's generic `size` object.
8942
- * This adapter does NOT auto-inject it; callers pass `providerOptions` when they
8943
- * need a specific size. Example:
8944
- * img.generate({
8945
- * provider: "fal",
8946
- * model: "fal-ai/gpt-image-1.5",
8947
- * prompt: "...",
8948
- * providerOptions: { fal: { image_size: "1024x1024" } },
8949
- * });
8950
- */
8951
- /**
8952
- * fal endpoint ids proven to work with this adapter and priced in
8953
- * {@link FAL_IMAGE_PRICE_USD} below (FLUX Pro Ultra + gpt-image).
8954
- */
8955
- const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
8956
- const FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
8957
- /** Env var read for the fal key when `apiKey` is not supplied in config. */
8958
- const FAL_API_KEY_ENV = "FAL_KEY";
8959
- /**
8960
- * Static fal USD/image, keyed by the fal ENDPOINT id. Cost tracking must price
8961
- * the endpoints consumers actually pass — kiln uses
8962
- * `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
8963
- * sourced from the fal registry snapshot in `../models`
8964
- * (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
8965
- * drifting to a second hand-maintained copy; the `??` fallbacks are the registry
8966
- * values at time of writing, used only if a registry entry loses its optional
8967
- * price. Each key is `MODELS[x].endpoint`; the trailing comment cites the
8968
- * `MODELS` key it comes from. Covers the flux/flux2 family, banana, seedream, and
8969
- * the other priced fal generation endpoints.
8970
- */
8971
- const FAL_IMAGE_PRICE_USD = {
8972
- [FAL_FLUX_MODEL]: MODELS.flux?.pricePerImageUsd ?? .06,
8973
- "fal-ai/flux/schnell": MODELS["flux-fast"]?.pricePerImageUsd ?? .003,
8974
- "fal-ai/flux-2-max": MODELS["flux2-max"]?.pricePerImageUsd ?? .07,
8975
- "fal-ai/flux-2-pro": MODELS["flux2-pro"]?.pricePerImageUsd ?? .03,
8976
- "fal-ai/flux-2-flex": MODELS["flux2-flex"]?.pricePerImageUsd ?? .05,
8977
- "fal-ai/flux-2": MODELS["flux2-dev"]?.pricePerImageUsd ?? .012,
8978
- "fal-ai/flux-2/turbo": MODELS["flux2-turbo"]?.pricePerImageUsd ?? .008,
8979
- [FAL_GPT_IMAGE_MODEL]: MODELS.gpt?.pricePerImageUsd ?? .133,
8980
- "openai/gpt-image-2": MODELS.gpt2?.pricePerImageUsd ?? .211,
8981
- "fal-ai/nano-banana-2": MODELS.banana2?.pricePerImageUsd ?? .08,
8982
- "fal-ai/nano-banana-pro": MODELS.banana?.pricePerImageUsd ?? .15,
8983
- "fal-ai/gemini-25-flash-image": MODELS.gemini?.pricePerImageUsd ?? .0398,
8984
- "fal-ai/gemini-3-pro-image-preview": MODELS.gemini3?.pricePerImageUsd ?? .15,
8985
- "fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
8986
- "fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
8987
- "bytedance/seedream/v5/pro/text-to-image": MODELS.seedream5?.pricePerImageUsd ?? .0675,
8988
- "bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
8989
- "fal-ai/recraft/v3/text-to-image": MODELS.recraft?.pricePerImageUsd ?? .04,
8990
- "fal-ai/recraft/v4/text-to-image": MODELS.recraft4?.pricePerImageUsd ?? .04,
8991
- "fal-ai/recraft/v4.1/text-to-image": MODELS.recraft41?.pricePerImageUsd ?? .035,
8992
- "fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
8993
- "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
8994
- "xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
8995
- "xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
8996
- "xai/grok-imagine-image/quality/text-to-image": MODELS["grok-image-quality"]?.pricePerImageUsd ?? .05,
8997
- "meta/muse-image/text-to-image": MODELS["muse-image"]?.pricePerImageUsd ?? .01,
8998
- "microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
8999
- "fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
9000
- "alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
9001
- "fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
9002
- };
9003
- /**
9004
- * Build a fal `ImageModel`. Prefers the passed `apiKey`, else the `FAL_KEY` env
9005
- * var. Throws `MotifError` when neither is present (callers translate this into
9006
- * a `Result.err`).
9007
- */
9008
- function resolveModel$3(modelId, apiKey, fetch) {
9009
- const key = apiKey ?? process.env["FAL_KEY"];
9010
- if (key === void 0 || key === "") throw new MotifError(`fal image generation requires an API key (config.fal.apiKey or ${FAL_API_KEY_ENV})`, 0);
9011
- return createFal({
9012
- apiKey: key,
9013
- ...toProviderFetch(fetch)
9014
- }).image(modelId);
9015
- }
9016
- /**
9017
- * How a registered fal edit endpoint takes its input images, from the
9018
- * registry's `editImagesField` (default `image_urls`). `@ai-sdk/fal` sends only
9019
- * the first image as `image_url` unless told otherwise, which list-only
9020
- * endpoints reject and which silently drops every reference after the first.
9021
- * Undefined for an endpoint the registry does not list as an edit route.
9022
- */
9023
- function falEditImagesField(modelId) {
9024
- const config = Object.values(MODELS).find((entry) => entry.editEndpoint === modelId);
9025
- return config === void 0 ? void 0 : config.editImagesField ?? "image_urls";
9026
- }
9027
- /** The fal provider adapter registered in the provider registry. */
9028
- const falAdapter = {
9029
- id: "fal",
9030
- apiKeyEnv: FAL_API_KEY_ENV,
9031
- resolveModel: resolveModel$3,
9032
- priceUsdByModel: FAL_IMAGE_PRICE_USD
9033
- };
9034
- //#endregion
9035
- //#region src/image/openai.ts
9036
- /**
9037
- * OpenAI (gpt-image) provider adapter.
9038
- *
9039
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/openai`. Building a model
9040
- * performs no network I/O — the request only happens when `generateImage`
9041
- * invokes `model.doGenerate`.
9042
- */
9043
- /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
9044
- const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
9045
- /**
9046
- * Static OpenAI-direct USD/image, keyed by model id.
9047
- *
9048
- * `gpt-image-1` ≈ $0.042/image at medium quality, 1024×1024. OpenAI bills image
9049
- * output by tokens, so the real figure varies with quality (low/medium/high)
9050
- * and size — this is a documented approximation for the common case.
9051
- * Source: https://platform.openai.com/docs/pricing (image generation)
9052
- * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
9053
- * (vs $0.133 via fal — full data and methodology on Linear MOT-23).
9054
- *
9055
- * GPT Image 2.6 is token-priced, with no published per-image estimate. Leave
9056
- * these models absent so cost remains unknown unless supplied by the provider.
9057
- * https://developers.openai.com/api/docs/models/gpt-image-2.6-flare
9058
- * https://developers.openai.com/api/docs/models/gpt-image-2.6-sunburst
9059
- */
9060
- const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
9061
- /**
9062
- * Build an OpenAI `ImageModel`. Prefers the passed `apiKey`, else the
9063
- * `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
9064
- * (callers translate this into a `Result.err`).
9065
- */
9066
- function resolveModel$2(modelId, apiKey, fetch) {
9067
- const key = apiKey ?? process.env["OPENAI_API_KEY"];
9068
- if (key === void 0 || key === "") throw new MotifError(`OpenAI image generation requires an API key (config.openai.apiKey or ${OPENAI_API_KEY_ENV})`, 0);
9069
- return createOpenAI({
9070
- apiKey: key,
9071
- ...toProviderFetch(fetch)
9072
- }).image(modelId);
9073
- }
9074
- /** The OpenAI provider adapter registered in the provider registry. */
9075
- const openaiAdapter = {
9076
- id: "openai",
9077
- apiKeyEnv: OPENAI_API_KEY_ENV,
9078
- resolveModel: resolveModel$2,
9079
- priceUsdByModel: OPENAI_IMAGE_PRICE_USD
9080
- };
9081
- //#endregion
9082
- //#region src/image/openrouter.ts
9083
- /** Env var read for the OpenRouter API key when `apiKey` is not supplied in config. */
9084
- const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
9085
- const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
9086
- /** Key under which this adapter's metadata sits on `providerMetadata`. */
9087
- const METADATA_KEY = "openrouter";
9088
- /**
9089
- * The Gemini image models Motif exposes, by their bare Google names, mapped to
9090
- * the OpenRouter slug that serves them. All six are listed by
9091
- * `GET https://openrouter.ai/api/v1/images/models`.
9092
- */
9093
- const OPENROUTER_GEMINI_IMAGE_MODELS = {
9094
- "gemini-2.5-flash-image": "google/gemini-2.5-flash-image",
9095
- "gemini-3.1-flash-image-preview": "google/gemini-3.1-flash-image-preview",
9096
- "gemini-3-pro-image-preview": "google/gemini-3-pro-image-preview",
9097
- "gemini-3.1-flash-image": "google/gemini-3.1-flash-image",
9098
- "gemini-3-pro-image": "google/gemini-3-pro-image",
9099
- "gemini-3.1-flash-lite-image": "google/gemini-3.1-flash-lite-image"
9100
- };
9101
- /**
9102
- * Static USD/image estimate, keyed by the bare model name. Used only when a
9103
- * response carries no `usage.cost`; OpenRouter normally reports the real figure.
9104
- *
9105
- * - `gemini-2.5-flash-image`: 1290 output tokens at $30 / 1M ≈ $0.039.
9106
- * - `gemini-3-pro-image-preview`, `gemini-3-pro-image`: ≈ $0.134 at 1K/2K.
9107
- * - `gemini-3.1-flash-image-preview`: priced with the 2.5 flash-image line
9108
- * pending a distinct published rate.
9109
- * - `gemini-3.1-flash-image`: $0.067 at 1K.
9110
- * - `gemini-3.1-flash-lite-image`: ≈ $0.0336 at 1K.
9111
- * Source: https://ai.google.dev/gemini-api/docs/pricing
9112
- */
9113
- const OPENROUTER_IMAGE_PRICE_USD = {
9114
- "gemini-2.5-flash-image": .039,
9115
- "gemini-3.1-flash-image-preview": .039,
9116
- "gemini-3-pro-image-preview": .134,
9117
- "gemini-3.1-flash-image": .067,
9118
- "gemini-3-pro-image": .134,
9119
- "gemini-3.1-flash-lite-image": .0336
9120
- };
9121
- /**
9122
- * Resolve a Motif model name to an OpenRouter slug. A bare Gemini name maps to
9123
- * its `google/<id>` slug; a name already containing `/` is an OpenRouter slug
9124
- * and passes through. Anything else is unknown and throws.
9125
- */
9126
- function openRouterModelSlug(modelId) {
9127
- if (modelId.includes("/")) return modelId;
9128
- const slug = OPENROUTER_GEMINI_IMAGE_MODELS[modelId];
9129
- 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);
9130
- return slug;
9131
- }
9132
- function isRecord$3(value) {
9133
- return typeof value === "object" && value !== null;
9134
- }
9135
- function toDataUrl(mediaType, data) {
9136
- return `data:${mediaType};base64,${typeof data === "string" ? data : Buffer.from(data).toString("base64")}`;
9137
- }
9138
- /** One `input_references` entry for an input file: a URL as-is, bytes as a data URL. */
9139
- function toInputReference(file) {
9140
- return {
9141
- type: "image_url",
9142
- image_url: { url: file.type === "url" ? file.url : toDataUrl(file.mediaType, file.data) }
9143
- };
9144
- }
9145
- function buildBody(slug, options) {
9146
- if (options.mask !== void 0) throw new MotifError("OpenRouter's Image API takes no mask; describe the region in the instruction instead.", 0);
9147
- return {
9148
- ...options.providerOptions[METADATA_KEY] ?? {},
9149
- model: slug,
9150
- prompt: options.prompt,
9151
- n: options.n,
9152
- ...options.aspectRatio === void 0 ? {} : { aspect_ratio: options.aspectRatio },
9153
- ...options.files === void 0 || options.files.length === 0 ? {} : { input_references: options.files.map(toInputReference) }
9154
- };
9155
- }
9156
- function errorMessage(body, fallback) {
9157
- if (isRecord$3(body) && isRecord$3(body.error)) {
9158
- const { message } = body.error;
9159
- if (typeof message === "string" && message !== "") return message;
9160
- }
9161
- return fallback;
9162
- }
9163
- function parseResponse(body) {
9164
- if (!isRecord$3(body) || !Array.isArray(body.data)) throw new MotifError("OpenRouter image response had no data array", 502);
9165
- const images = [];
9166
- for (const entry of body.data) if (isRecord$3(entry) && typeof entry.b64_json === "string") images.push(entry.b64_json);
9167
- if (images.length === 0) throw new MotifError("OpenRouter returned no images", 502);
9168
- return {
9169
- images,
9170
- cost: isRecord$3(body.usage) && typeof body.usage.cost === "number" ? body.usage.cost : void 0
9171
- };
9172
- }
9173
- function buildModel(modelId, apiKey, doFetch) {
9174
- const slug = openRouterModelSlug(modelId);
9175
- return {
9176
- specificationVersion: "v4",
9177
- provider: METADATA_KEY,
9178
- modelId: slug,
9179
- maxImagesPerCall: 10,
9180
- async doGenerate(options) {
9181
- const warnings = [];
9182
- if (options.seed !== void 0) warnings.push({
9183
- type: "unsupported",
9184
- feature: "seed"
9185
- });
9186
- if (options.size !== void 0) warnings.push({
9187
- type: "unsupported",
9188
- feature: "size",
9189
- details: "Gemini on OpenRouter takes aspectRatio, not pixel sizes."
9190
- });
9191
- const timestamp = /* @__PURE__ */ new Date();
9192
- const response = await doFetch(OPENROUTER_IMAGES_URL, {
9193
- method: "POST",
9194
- headers: {
9195
- ...options.headers,
9196
- Authorization: `Bearer ${apiKey}`,
9197
- "Content-Type": "application/json"
9198
- },
9199
- body: JSON.stringify(buildBody(slug, options)),
9200
- ...options.abortSignal === void 0 ? {} : { signal: options.abortSignal }
9201
- });
9202
- const text = await response.text();
9203
- let body;
9204
- try {
9205
- body = JSON.parse(text);
9206
- } catch {
9207
- body = void 0;
9208
- }
9209
- if (!response.ok) throw new MotifError(errorMessage(body, `OpenRouter ${response.status}: ${text}`), response.status);
9210
- const { images, cost } = parseResponse(body);
9211
- return {
9212
- images,
9213
- warnings,
9214
- providerMetadata: { [METADATA_KEY]: {
9215
- images: images.map(() => ({})),
9216
- ...cost === void 0 ? {} : { cost }
9217
- } },
9218
- response: {
9219
- timestamp,
9220
- modelId: slug,
9221
- headers: Object.fromEntries(response.headers.entries())
9222
- }
9223
- };
9224
- }
9225
- };
9226
- }
9227
- /**
9228
- * Build an OpenRouter `ImageModel`. Prefers the passed `apiKey`, else the
9229
- * `OPENROUTER_API_KEY` env var. Throws `MotifError` when neither is present or
9230
- * the model name is unknown (callers translate this into a `Result.err`).
9231
- */
9232
- function resolveModel$1(modelId, apiKey, fetch) {
9233
- const key = apiKey ?? process.env["OPENROUTER_API_KEY"];
9234
- if (key === void 0 || key === "") throw new MotifError(`OpenRouter image generation requires an API key (config.openrouter.apiKey or ${OPENROUTER_API_KEY_ENV})`, 0);
9235
- const configured = toProviderFetch(fetch);
9236
- return buildModel(modelId, key, "fetch" in configured ? configured.fetch : globalThis.fetch);
9237
- }
9238
- /** The OpenRouter provider adapter registered in the provider registry. */
9239
- const openrouterAdapter = {
9240
- id: "openrouter",
9241
- apiKeyEnv: OPENROUTER_API_KEY_ENV,
9242
- resolveModel: resolveModel$1,
9243
- priceUsdByModel: OPENROUTER_IMAGE_PRICE_USD
9244
- };
9245
- //#endregion
9246
- //#region src/image/replicate.ts
9247
- /**
9248
- * Replicate provider adapter.
9249
- *
9250
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/replicate`. Building a model
9251
- * performs no network I/O — the request only happens when `generateImage`
9252
- * invokes `model.doGenerate`.
9253
- *
9254
- * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
9255
- * and reads `REPLICATE_API_TOKEN` from the environment.
9256
- */
9257
- /** Env var read for the Replicate API token when `apiToken` is not in config. */
9258
- const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
9259
- /**
9260
- * Static Replicate USD/image, keyed by model id.
9261
- *
9262
- * `black-forest-labs/flux-1.1-pro-ultra` = $0.06/image (Replicate list price).
9263
- * Source: https://replicate.com/black-forest-labs/flux-1.1-pro-ultra
9264
- * Sanity anchor: fal's `fal-ai/flux-pro/v1.1-ultra` is also $0.06
9265
- * (`MODELS.flux.pricePerImageUsd` in ../models) — same price, both routes.
9266
- */
9267
- const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06 };
9268
- /**
9269
- * Build a Replicate `ImageModel`. Prefers the passed `apiKey` (the normalized
9270
- * token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
9271
- * neither is present (callers translate this into a `Result.err`).
9272
- */
9273
- function resolveModel(modelId, apiKey, fetch) {
9274
- const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
9275
- if (token === void 0 || token === "") throw new MotifError(`Replicate image generation requires an API token (config.replicate.apiToken or ${REPLICATE_API_KEY_ENV})`, 0);
9276
- return createReplicate({
9277
- apiToken: token,
9278
- ...toProviderFetch(fetch)
9279
- }).image(modelId);
9280
- }
9281
- //#endregion
9282
- //#region src/image/provider.ts
9283
- /**
9284
- * The provider registry. Every id in {@link ImageProviderId}'s closed part maps
9285
- * to its adapter; the open union tail means a lookup can still miss, so access
9286
- * goes through {@link getProviderAdapter}.
9287
- */
9288
- const PROVIDERS = {
9289
- openrouter: openrouterAdapter,
9290
- openai: openaiAdapter,
9291
- replicate: {
9292
- id: "replicate",
9293
- apiKeyEnv: REPLICATE_API_KEY_ENV,
9294
- resolveModel,
9295
- priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
9296
- },
9297
- fal: falAdapter
9298
- };
9299
- /**
9300
- * Look up an adapter by provider id, throwing a `MotifError` for an unknown
9301
- * provider (callers catch this into a `Result.err`).
9302
- */
9303
- function getProviderAdapter(provider) {
9304
- const adapter = PROVIDERS[provider];
9305
- if (adapter === void 0) throw new MotifError(`Unsupported image provider: ${provider}`, 0);
9306
- return adapter;
9307
- }
9308
- //#endregion
9309
- //#region src/image/cost.ts
9310
- /**
9311
- * Per-call cost tracking for the image layer.
9312
- *
9313
- * Preference order:
9314
- * 1. A cost surfaced by the provider on `result.providerMetadata` (most image
9315
- * providers do NOT surface one today, so this is usually absent).
9316
- * 2. A static per-model table, owned per-adapter (`priceUsdByModel`) and read
9317
- * from the provider registry (see sources in each adapter).
9318
- * 3. Unknown → `{ usd: 0, source: "unknown" }`.
9319
- */
9320
- function isRecord$2(value) {
9321
- return typeof value === "object" && value !== null;
9322
- }
9323
- /**
9324
- * Extract a provider-reported total cost from a `generateImage` result's
9325
- * `providerMetadata`, if the provider surfaces a numeric `cost`. The shape is
9326
- * `{ [provider]: { ...; cost?: number } }`. Returns undefined otherwise.
9327
- */
9328
- function costFromProviderMetadata(providerMetadata) {
9329
- if (!isRecord$2(providerMetadata)) return;
9330
- for (const value of Object.values(providerMetadata)) if (isRecord$2(value)) {
9331
- const cost = value.cost;
9332
- if (typeof cost === "number" && Number.isFinite(cost)) return cost;
9333
- }
9334
- }
9335
- function tablePricePerImage(provider, modelId) {
9336
- const adapter = PROVIDERS[provider];
9337
- if (adapter === void 0) return;
9338
- return adapter.priceUsdByModel[modelId];
9339
- }
9340
- function roundUsd(value) {
9341
- return Number(value.toFixed(6));
9342
- }
9343
- /**
9344
- * Cost across every underlying model call of one generation. Provider-metadata
9345
- * costs from the calls that report one are summed; otherwise the static table
9346
- * (× image count), then unknown.
9347
- */
9348
- function costForCalls(provider, modelId, callMetadata, imageCount) {
9349
- let metaCost;
9350
- for (const providerMetadata of callMetadata) {
9351
- const callCost = costFromProviderMetadata(providerMetadata);
9352
- if (callCost !== void 0) metaCost = (metaCost ?? 0) + callCost;
9353
- }
9354
- if (metaCost !== void 0) return {
9355
- usd: roundUsd(metaCost),
9356
- source: "provider-metadata"
9357
- };
9358
- const perImage = tablePricePerImage(provider, modelId);
9359
- if (perImage !== void 0) return {
9360
- usd: roundUsd(perImage * Math.max(imageCount, 1)),
9361
- source: "table"
9362
- };
9363
- return {
9364
- usd: 0,
9365
- source: "unknown"
9366
- };
9367
- }
9368
- //#endregion
9369
- //#region src/image/index.ts
9370
- /**
9371
- * `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
9372
- *
9373
- * ESM-only subpath export, built on the Vercel AI SDK image interface
9374
- * (`generateImage`, `@ai-sdk/*`). The caller names the provider and model;
9375
- * reuses the SDK's Result convention (`Result<T, MotifError>` — no
9376
- * thrown exceptions). Gemini is reached through OpenRouter.
9377
- *
9378
- * @example
9379
- * ```ts
9380
- * import { createMotifImage } from "@howells/motif-sdk/image";
9381
- *
9382
- * const img = createMotifImage({ defaultProvider: "openrouter" });
9383
- * const r = await img.generate({ model: "gemini-3.1-flash-image", prompt: "a bare concrete wall" });
9384
- * if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
9385
- * ```
9386
- */
9387
- const DEFAULT_PROVIDER = "openrouter";
9388
- /**
9389
- * Create a provider-agnostic image client.
9390
- *
9391
- * @param config - provider selection + keys (keys fall back to env).
9392
- * @param deps - INTERNAL testing seam. Defaults to the real AI SDK `generateImage`
9393
- * and the built-in provider adapters; tests inject fakes here to run offline.
9394
- */
9395
- function createMotifImage(config = {}, deps = {}) {
9396
- const generateImageFn = deps.generateImage ?? generateImage;
9397
- const resolveModelFn = deps.resolveModel ?? defaultResolveModel;
9398
- function resolveProvider(provider) {
9399
- return provider ?? config.defaultProvider ?? DEFAULT_PROVIDER;
9400
- }
9401
- function apiKeyFor(provider) {
9402
- switch (provider) {
9403
- case "openrouter": return config.openrouter?.apiKey;
9404
- case "openai": return config.openai?.apiKey;
9405
- case "replicate": return config.replicate?.apiToken;
9406
- case "fal": return config.fal?.apiKey;
9407
- default: return;
9408
- }
9409
- }
9410
- async function generate(opts) {
9411
- const provider = resolveProvider(opts.provider);
9412
- try {
9413
- const modelId = opts.model;
9414
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
9415
- const result = await generateImageFn({
9416
- model,
9417
- prompt: opts.prompt,
9418
- ...opts.n === void 0 ? {} : { n: opts.n },
9419
- ...opts.size === void 0 ? {} : { size: opts.size },
9420
- ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
9421
- ...opts.seed === void 0 ? {} : { seed: opts.seed },
9422
- ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
9423
- ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
9424
- ...opts.headers === void 0 ? {} : { headers: opts.headers },
9425
- ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
9426
- });
9427
- return ok$1(toMotifImageResult(result, provider, modelId));
9428
- } catch (error) {
9429
- return err$1(toMotifError(error));
9430
- }
9431
- }
9432
- async function edit(opts) {
9433
- const provider = resolveProvider(opts.provider);
9434
- const imagesField = provider === "fal" ? falEditImagesField(opts.model) : void 0;
9435
- if (imagesField === "image_url" && opts.images.length > 1) return err$1(new MotifError(`${opts.model} takes one input image; ${opts.images.length} were given`, 0));
9436
- const providerOptions = imagesField === "image_urls" && opts.providerOptions?.fal?.useMultipleImages === void 0 ? {
9437
- ...opts.providerOptions,
9438
- fal: {
9439
- ...opts.providerOptions?.fal,
9440
- useMultipleImages: true
9441
- }
9442
- } : opts.providerOptions;
9443
- try {
9444
- const modelId = opts.model;
9445
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
9446
- const result = await generateImageFn({
9447
- model,
9448
- prompt: {
9449
- images: opts.images,
9450
- text: opts.instruction,
9451
- ...opts.mask === void 0 ? {} : { mask: opts.mask }
9452
- },
9453
- ...opts.n === void 0 ? {} : { n: opts.n },
9454
- ...opts.seed === void 0 ? {} : { seed: opts.seed },
9455
- ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
9456
- ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
9457
- ...opts.headers === void 0 ? {} : { headers: opts.headers },
9458
- ...providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(providerOptions) }
9459
- });
9460
- return ok$1(toMotifImageResult(result, provider, modelId));
9461
- } catch (error) {
9462
- return err$1(toMotifError(error));
9463
- }
9464
- }
9465
- async function bestOfN(opts) {
9466
- return await runBestOfN(opts, generate, edit);
9467
- }
9468
- return {
9469
- generate,
9470
- edit,
9471
- bestOfN
9472
- };
9473
- }
9474
- /**
9475
- * Fire one best-of-N candidate: reuse `generate`/`edit` (discriminated by the
9476
- * presence of `images`, narrowed inline so TS refines the union). Each candidate
9477
- * makes a single image (`n: 1`) — the outer `n` is the candidate count, not the
9478
- * per-call image count — and takes a distinct `seed` (`seed + index`) when a seed
9479
- * was given, so the N vary. `opts.signal` rides along via the spread, so one
9480
- * cancel aborts every candidate.
9481
- */
9482
- async function runCandidate(opts, index, generate, edit) {
9483
- const candidateSeed = opts.seed === void 0 ? void 0 : opts.seed + index;
9484
- const overrides = {
9485
- n: 1,
9486
- ...candidateSeed === void 0 ? {} : { seed: candidateSeed }
9487
- };
9488
- if ("images" in opts) return await edit({
9489
- ...opts,
9490
- ...overrides
9491
- });
9492
- return await generate({
9493
- ...opts,
9494
- ...overrides
9495
- });
9496
- }
9497
- /** Ask the judge to pick a winner and validate its index; no judge → index 0. */
9498
- async function selectWinner(opts, candidates) {
9499
- const { judge } = opts;
9500
- if (judge === void 0) return ok$1({ chosenIndex: 0 });
9501
- const decision = await judge(candidates, "images" in opts ? { instruction: opts.instruction } : { prompt: opts.prompt });
9502
- if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= candidates.length) return err$1(new MotifError(`bestOfN judge returned an out-of-range index ${decision.index} (expected 0..${candidates.length - 1})`, 0));
9503
- return ok$1({
9504
- chosenIndex: decision.index,
9505
- ...decision.reason === void 0 ? {} : { reason: decision.reason }
9506
- });
9507
- }
9508
- /**
9509
- * Best-of-N: generate `n` candidates in parallel, keep the successes, and pick a
9510
- * winner via the (optional, injectable) judge. Wraps everything so no throw
9511
- * escapes — any failure becomes a `Result.err`.
9512
- */
9513
- async function runBestOfN(opts, generate, edit) {
9514
- try {
9515
- const { n } = opts;
9516
- if (!Number.isInteger(n) || n < 1) return err$1(new MotifError(`bestOfN requires an integer n >= 1 (got ${n})`, 0));
9517
- const results = await Promise.all(Array.from({ length: n }, (_unused, index) => index).map(async (index) => await runCandidate(opts, index, generate, edit)));
9518
- const candidates = [];
9519
- let firstError;
9520
- for (const result of results) if (result.isOk()) candidates.push(result.value);
9521
- else firstError ??= result.error;
9522
- if (candidates.length === 0) return err$1(firstError ?? new MotifError(`all ${n} bestOfN candidates failed`, 0));
9523
- const winner = await selectWinner(opts, candidates);
9524
- if (winner.isErr()) return err$1(winner.error);
9525
- const { chosenIndex, reason } = winner.value;
9526
- const best = candidates[chosenIndex];
9527
- if (best === void 0) return err$1(new MotifError("bestOfN failed to resolve a winner", 0));
9528
- const totalCostUsd = Number(candidates.reduce((sum, candidate) => sum + candidate.cost.usd, 0).toFixed(6));
9529
- return ok$1({
9530
- best,
9531
- chosenIndex,
9532
- ...reason === void 0 ? {} : { reason },
9533
- candidates,
9534
- totalCostUsd
9535
- });
9536
- } catch (error) {
9537
- return err$1(toMotifError(error));
9538
- }
9539
- }
9540
- /** Default provider dispatch: resolve the model through the provider registry. */
9541
- function defaultResolveModel(provider, modelId, apiKey, fetch) {
9542
- return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
9543
- }
9544
- /** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
9545
- function toMotifImageResult(result, provider, model) {
9546
- const images = result.images.map((file) => ({
9547
- uint8Array: file.uint8Array,
9548
- base64: file.base64,
9549
- mediaType: file.mediaType
9550
- }));
9551
- const cost = costForCalls(provider, model, result.calls.map((call) => call.providerMetadata), images.length);
9552
- const requestId = extractRequestId(result);
9553
- const warnings = result.warnings.map(renderWarning);
9554
- return {
9555
- images,
9556
- cost,
9557
- provider,
9558
- model,
9559
- ...requestId === void 0 ? {} : { requestId },
9560
- ...warnings.length === 0 ? {} : { warnings }
9561
- };
9562
- }
9563
- /** Render an AI SDK `Warning` object into a readable one-line string. */
9564
- function renderWarning(warning) {
9565
- if (warning.type === "unsupported" || warning.type === "compatibility") {
9566
- const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
9567
- return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
9568
- }
9569
- if (warning.type === "deprecated") return `deprecated setting "${warning.setting}": ${warning.message}`;
9570
- return warning.message;
9571
- }
9572
- function isRecord$1(value) {
9573
- return typeof value === "object" && value !== null;
9574
- }
9575
- /** Look for a provider correlation id in providerMetadata, then response headers. */
9576
- function extractRequestId(result) {
9577
- for (const call of result.calls) {
9578
- const fromMetadata = requestIdFromMetadata(call.providerMetadata);
9579
- if (fromMetadata !== void 0) return fromMetadata;
9580
- }
9581
- for (const { response } of result.calls) {
9582
- const { headers } = response;
9583
- if (headers) {
9584
- const id = headers["x-request-id"] ?? headers["x-goog-request-id"] ?? headers["x-fal-request-id"];
9585
- if (typeof id === "string" && id !== "") return id;
9586
- }
9587
- }
9588
- }
9589
- function requestIdFromMetadata(providerMetadata) {
9590
- if (!isRecord$1(providerMetadata)) return;
9591
- for (const value of Object.values(providerMetadata)) if (isRecord$1(value)) {
9592
- const id = value.requestId ?? value.request_id;
9593
- if (typeof id === "string" && id !== "") return id;
9594
- }
9595
- }
9596
- /**
9597
- * Coerce a public `providerOptions` (values typed `unknown`) into the AI SDK's
9598
- * JSON-only `Record<string, Record<string, JSONValue>>`. Fails fast: a
9599
- * non-JSON-representable value (undefined/function/bigint/symbol) throws a
9600
- * `MotifError` naming the offending path rather than silently forwarding a value
9601
- * the caller did not intend.
9602
- */
9603
- function toProviderOptions(input) {
9604
- const out = {};
9605
- for (const [namespace, options] of Object.entries(input)) {
9606
- const inner = {};
9607
- for (const [key, value] of Object.entries(options)) inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
9608
- out[namespace] = inner;
9609
- }
9610
- return out;
9611
- }
9612
- function toJsonValue(value, path) {
9613
- if (value === null) return null;
9614
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9615
- if (typeof value === "object") {
9616
- if (Array.isArray(value)) {
9617
- const arr = [];
9618
- for (const [index, item] of value.entries()) arr.push(toJsonValue(item, `${path}[${index}]`));
9619
- return arr;
9620
- }
9621
- const obj = {};
9622
- for (const [key, entry] of Object.entries(value)) obj[key] = toJsonValue(entry, `${path}.${key}`);
9623
- return obj;
9624
- }
9625
- throw new MotifError(`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`, 0);
9626
- }
9627
- /**
9628
- * Coerce an unknown thrown value into a `MotifError`, mirroring server.ts's
9629
- * mapping: preserve an existing `MotifError`, lift a string `code`, and mark
9630
- * non-HTTP local failures with status `0`.
9631
- */
9632
- function toMotifError(error) {
9633
- if (error instanceof MotifError) return error;
9634
- const message = error instanceof Error ? error.message : String(error);
9635
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
9636
- const status = error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0;
9637
- const body = error instanceof Error && "responseBody" in error && typeof error.responseBody === "string" ? error.responseBody : message;
9638
- if (isFalAccountLocked(status, body)) return falHttpError(status, body);
9639
- return new MotifError(message, status, code);
9640
- }
9641
- //#endregion
9642
8793
  //#region src/task-output.ts
9643
8794
  function isPlainRecord(value) {
9644
8795
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -9932,14 +9083,14 @@ function projectedOutputs(model, input, body) {
9932
9083
  }
9933
9084
  function outputSize(parameters, input, imageSize) {
9934
9085
  if (typeof imageSize === "string") return FAL_PRESET_SIZES[imageSize];
9935
- if (isPixelSize$1(imageSize)) return imageSize;
9086
+ if (isPixelSize(imageSize)) return imageSize;
9936
9087
  if (imageSize === void 0) {
9937
9088
  const fallback = parameters.find((parameter) => parameter.key === "image_size")?.fallback;
9938
9089
  if (fallback === void 0) return sourceSizeOf(input);
9939
9090
  return typeof fallback === "string" ? FAL_PRESET_SIZES[fallback] : void 0;
9940
9091
  }
9941
9092
  }
9942
- function isPixelSize$1(value) {
9093
+ function isPixelSize(value) {
9943
9094
  return typeof value === "object" && value !== null && "width" in value && "height" in value && typeof value.width === "number" && typeof value.height === "number";
9944
9095
  }
9945
9096
  /** What the plan knows of the source, for prices that read it. */
@@ -10325,7 +9476,6 @@ function taskRequest(task, input) {
10325
9476
  function resolve(task, input, context, options) {
10326
9477
  const keys = [];
10327
9478
  if (options.dryRun === true || context.falKey !== void 0) keys.push("FAL_KEY");
10328
- if (options.dryRun === true || context.openAiKey !== void 0) keys.push("OPENAI_API_KEY");
10329
9479
  const resolution = resolveTask(task, taskRequest(task, input), {
10330
9480
  keys,
10331
9481
  pins: context.pins
@@ -10400,7 +9550,7 @@ function withCode(error, model, task) {
10400
9550
  field: error.field,
10401
9551
  task
10402
9552
  });
10403
- const motif = toMotifError$1(error);
9553
+ const motif = toMotifError(error);
10404
9554
  if (motif.code !== void 0) return motif;
10405
9555
  return new MotifError(motif.message, 0, INVALID_OPTION, void 0, {
10406
9556
  model,
@@ -10498,7 +9648,6 @@ function generationPlan(task, model, config, input) {
10498
9648
  field: "mask",
10499
9649
  requires: "references"
10500
9650
  }));
10501
- if (input.transparent === true && config.supportsBackground !== true && config.transparencyRoute !== void 0) return openAiPlan(task, model, config.transparencyRoute, input, editImageUrls);
10502
9651
  if (input.outputFormat !== void 0 && !outputFormatsOf(config, editImageUrls.length > 0).includes(input.outputFormat)) return err$1(cannotCarry("outputFormat", model, task));
10503
9652
  const { quality, ...params } = input.params ?? {};
10504
9653
  const liftQuality = config.supportsQuality === true && quality !== void 0;
@@ -10541,55 +9690,6 @@ function generationPlan(task, model, config, input) {
10541
9690
  queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
10542
9691
  });
10543
9692
  }
10544
- /**
10545
- * The OpenAI transparency route. Ported from the CLI's generate-openai: the
10546
- * route carries only what gpt-image takes, so everything else is refused.
10547
- */
10548
- function openAiPlan(task, model, route, input, editImageUrls) {
10549
- const allowed = /* @__PURE__ */ new Set([
10550
- "aspect",
10551
- "count",
10552
- "look",
10553
- "mask",
10554
- "mood",
10555
- "prompt",
10556
- "references",
10557
- "transparent"
10558
- ]);
10559
- if (task === "vary") allowed.add("image");
10560
- if (input.outputFormat === "png") allowed.add("outputFormat");
10561
- const refused = refuseOthers(input, allowed, model, task);
10562
- if (refused !== void 0) return err$1(refused);
10563
- if (input.params !== void 0) return err$1(cannotCarry("params", model, task));
10564
- const editing = editImageUrls.length > 0;
10565
- if (editing && !route.supportsEdit) return err$1(cannotCarry("references", model, task));
10566
- let prompt;
10567
- try {
10568
- const creative = creativeOf(input);
10569
- prompt = creative === void 0 ? input.prompt ?? "" : enrichPrompt({
10570
- creative,
10571
- prompt: input.prompt ?? ""
10572
- }).prompt;
10573
- } catch (error) {
10574
- return err$1(withCode(error, model, task));
10575
- }
10576
- return ok$1({
10577
- body: {
10578
- n: input.count ?? 1,
10579
- prompt,
10580
- size: aspectToGptSize(input.aspect ?? "1:1"),
10581
- ...editing && { images: [...editImageUrls] },
10582
- ...input.mask !== void 0 && { mask: input.mask },
10583
- background: "transparent",
10584
- outputFormat: "png"
10585
- },
10586
- cost: UNKNOWN_COST,
10587
- endpoint: `${route.provider}:${route.model}`,
10588
- prompt,
10589
- provider: "openai",
10590
- queued: false
10591
- });
10592
- }
10593
9693
  function flatPrice(pricing) {
10594
9694
  const match = FLAT_PRICE_REGEX.exec(pricing.trim());
10595
9695
  return match === null ? null : Number(match[1]);
@@ -10641,7 +9741,7 @@ function parsePayload(text) {
10641
9741
  if (JSON_START.test(text.trim())) throw error;
10642
9742
  raw = text;
10643
9743
  }
10644
- if (!isRecord$4(raw) || Array.isArray(raw)) return {
9744
+ if (!isRecord$1(raw) || Array.isArray(raw)) return {
10645
9745
  raw,
10646
9746
  files: []
10647
9747
  };
@@ -10914,8 +10014,8 @@ function enforceBufferLimit(length, requestId) {
10914
10014
  /**
10915
10015
  * The Task client: one function per Task. `createMotif()` returns a client
10916
10016
  * whose `generate`, `erase`, `upscale` and the rest resolve the Model through
10917
- * `resolveTask`, build the request that Model needs, run it on fal (or OpenAI
10918
- * for a transparency route) and return the files and fields it produced.
10017
+ * `resolveTask`, build the request that Model needs, run it on fal and return
10018
+ * the files and fields it produced.
10919
10019
  *
10920
10020
  * @example
10921
10021
  * ```ts
@@ -10925,7 +10025,6 @@ function enforceBufferLimit(length, requestId) {
10925
10025
  * ```
10926
10026
  */
10927
10027
  const MISSING_API_KEY = "MISSING_API_KEY";
10928
- const PIXEL_SIZE_REGEX = /^\d+x\d+$/;
10929
10028
  function presentKey(value) {
10930
10029
  return value === void 0 || value === "" ? void 0 : value;
10931
10030
  }
@@ -10943,9 +10042,6 @@ function outputKeysFor(model, body) {
10943
10042
  if (model === "clarity" || model === "crystal") return ["image"];
10944
10043
  return ["images"];
10945
10044
  }
10946
- function isPixelSize(size) {
10947
- return PIXEL_SIZE_REGEX.test(size);
10948
- }
10949
10045
  function isRecord(value) {
10950
10046
  return typeof value === "object" && value !== null;
10951
10047
  }
@@ -10996,106 +10092,6 @@ function falOutput(plan, result) {
10996
10092
  tier: plan.tier
10997
10093
  };
10998
10094
  }
10999
- const DEFAULT_TIMEOUT_MS = 12e4;
11000
- /**
11001
- * Remote images as bytes, fetched through the configured seam: the AI SDK
11002
- * would otherwise download them with global fetch. Data URLs pass through.
11003
- */
11004
- async function localImages(sources, fetch) {
11005
- const images = [];
11006
- for (const source of sources) {
11007
- if (!source.startsWith("https://") && !source.startsWith("http://")) {
11008
- images.push(source);
11009
- continue;
11010
- }
11011
- try {
11012
- const response = await fetch(source, { method: "GET" });
11013
- if (!response.ok) return err$1(new MotifError(`Could not download ${source}: ${response.status}`, response.status, "DOWNLOAD_FAILED"));
11014
- images.push(new Uint8Array(await response.arrayBuffer()));
11015
- } catch (error) {
11016
- return err$1(new MotifError(`Could not download ${source}: ${error instanceof Error ? error.message : String(error)}`, 0, "DOWNLOAD_FAILED"));
11017
- }
11018
- }
11019
- return ok$1(images);
11020
- }
11021
- async function openAiOutput(plan, openAiKey, config) {
11022
- const fetch = config.fetch ?? (async (url, init) => await globalThis.fetch(url, init));
11023
- const client = createMotifImage({
11024
- defaultProvider: "openai",
11025
- fetch,
11026
- openai: { apiKey: openAiKey },
11027
- ...config.retries !== void 0 && { maxRetries: config.retries }
11028
- });
11029
- const signal = AbortSignal.timeout(config.timeout ?? DEFAULT_TIMEOUT_MS);
11030
- const { body } = plan;
11031
- const [provider, model = ""] = plan.endpoint.split(":");
11032
- const n = typeof body.n === "number" ? body.n : 1;
11033
- const prompt = typeof body.prompt === "string" ? body.prompt : "";
11034
- const providerOptions = { openai: {
11035
- background: "transparent",
11036
- outputFormat: "png"
11037
- } };
11038
- const sources = Array.isArray(body.images) ? body.images.filter((item) => typeof item === "string") : [];
11039
- const mask = typeof body.mask === "string" ? [body.mask] : [];
11040
- const downloaded = await localImages([...sources, ...mask], fetch);
11041
- if (downloaded.isErr()) return err$1(downloaded.error);
11042
- const images = downloaded.value.slice(0, sources.length);
11043
- const maskImage = downloaded.value[sources.length];
11044
- const size = typeof body.size === "string" ? body.size : "auto";
11045
- const common = {
11046
- model,
11047
- n,
11048
- provider,
11049
- providerOptions,
11050
- signal
11051
- };
11052
- const result = images.length === 0 ? await client.generate({
11053
- ...common,
11054
- prompt,
11055
- ...isPixelSize(size) && { size }
11056
- }) : await client.edit({
11057
- ...common,
11058
- images,
11059
- instruction: prompt,
11060
- ...maskImage !== void 0 && { mask: maskImage }
11061
- });
11062
- if (result.isErr()) return err$1(result.error);
11063
- const value = result.value;
11064
- let cost = {
11065
- basis: "unknown",
11066
- usd: null
11067
- };
11068
- if (value.cost.source === "provider-metadata") cost = {
11069
- basis: "measured",
11070
- usd: value.cost.usd
11071
- };
11072
- else if (value.cost.source === "table") cost = {
11073
- basis: "projected",
11074
- usd: value.cost.usd
11075
- };
11076
- return ok$1({
11077
- chosenBy: plan.chosenBy,
11078
- cost,
11079
- data: {},
11080
- endpoint: plan.endpoint,
11081
- files: value.images.map((image) => ({
11082
- key: "images",
11083
- url: `data:${image.mediaType};base64,${image.base64}`
11084
- })),
11085
- model: plan.model,
11086
- ...plan.mode !== void 0 && { mode: plan.mode },
11087
- ...plan.prompt !== void 0 && { prompt: plan.prompt },
11088
- provider: plan.provider,
11089
- raw: {
11090
- model: value.model,
11091
- provider: value.provider,
11092
- ...value.warnings !== void 0 && { warnings: value.warnings }
11093
- },
11094
- ...value.requestId !== void 0 && { requestId: value.requestId },
11095
- task: plan.task,
11096
- tier: plan.tier
11097
- });
11098
- }
11099
10095
  function taskStreamer(config, plan, context) {
11100
10096
  return async (task, input, options = {}) => {
11101
10097
  const planned = plan(task, input);
@@ -11106,12 +10102,11 @@ function taskStreamer(config, plan, context) {
11106
10102
  });
11107
10103
  };
11108
10104
  }
11109
- /** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
10105
+ /** Create the Task client. The key falls back to FAL_KEY. */
11110
10106
  function createMotif(config = {}) {
11111
10107
  function context() {
11112
10108
  return {
11113
10109
  falKey: presentKey(config.falKey ?? getFalKeyFromEnv()),
11114
- openAiKey: presentKey(config.openAiKey ?? getOpenAiKeyFromEnv()),
11115
10110
  pins: config.pins
11116
10111
  };
11117
10112
  }
@@ -11134,11 +10129,6 @@ function createMotif(config = {}) {
11134
10129
  const planned = plan(task, input);
11135
10130
  if (planned.isErr()) return err$1(planned.error);
11136
10131
  const chosen = planned.value;
11137
- if (chosen.provider === "openai") {
11138
- const openAiKey = context().openAiKey;
11139
- if (openAiKey === void 0) return err$1(new MotifError("OPENAI_API_KEY is not set.", 0, MISSING_API_KEY, void 0, { envVar: "OPENAI_API_KEY" }));
11140
- return await openAiOutput(chosen, openAiKey, config);
11141
- }
11142
10132
  const headers = input.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
11143
10133
  const prepared = {
11144
10134
  body: chosen.body,
@@ -11152,7 +10142,6 @@ function createMotif(config = {}) {
11152
10142
  const planned = plan(task, input);
11153
10143
  if (planned.isErr()) return err$1(planned.error);
11154
10144
  const chosen = planned.value;
11155
- if (chosen.provider !== "fal") return err$1(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
11156
10145
  const client = falClient();
11157
10146
  if (client.isErr()) return err$1(client.error);
11158
10147
  return (await awaitQueuedRequest(falRequestExecutor(client.value), {
@@ -11198,4 +10187,4 @@ function createMotif(config = {}) {
11198
10187
  };
11199
10188
  }
11200
10189
  //#endregion
11201
- export { ACCOUNT_LOCKED, ASPECT_RATIOS, CREATIVE_FIELDS, CREATIVE_TAXONOMY, CreativeOptionError, DEFAULT_TIER, FORMAT_PRESETS, INVALID_OPTION, ImageSizeBoundsError, LOOKS, MotifError, NO_MODEL_AVAILABLE, RESOLUTIONS, TASKS, TASK_IDS, TIERS, createMotif, enrichPrompt, err, formatCost, getFalKeyFromEnv, getLook, getOpenAiKeyFromEnv, isFalAccountLocked, isTaskId, modelProfile, motifEnvSchema, ok, parseMotifEnv, promptWarnings, resolveTask, sanitizePrompt, sumCosts, tierChangesChoice, validateCreativeDirection };
10190
+ export { ACCOUNT_LOCKED, ASPECT_RATIOS, CREATIVE_FIELDS, CREATIVE_TAXONOMY, CreativeOptionError, DEFAULT_TIER, FORMAT_PRESETS, INVALID_OPTION, ImageSizeBoundsError, LOOKS, MotifError, NO_MODEL_AVAILABLE, RESOLUTIONS, TASKS, TASK_IDS, TIERS, createMotif, enrichPrompt, err, formatCost, getFalKeyFromEnv, getLook, isFalAccountLocked, isTaskId, modelProfile, motifEnvSchema, ok, parseMotifEnv, promptWarnings, resolveTask, sanitizePrompt, sumCosts, tierChangesChoice, validateCreativeDirection };