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