openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
587 lines (586 loc) • 22.7 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { i as readResponseWithLimit } from "./http-response-body-L4SAGcXJ.mjs";
import { m as readProviderJsonResponse, n as assertOkOrThrowHttpError, r as assertOkOrThrowProviderError } from "./provider-http-errors-xcK_t3aM.mjs";
import { i as fetchWithSsrFGuard } from "./fetch-guard-CIAMeSj8.mjs";
import "./string-coerce-runtime-B1b8UNzb.mjs";
import { n as resolveGeneratedMediaMaxBytes } from "./configured-max-bytes-X3NTOxEj.mjs";
import { g as resolveProviderOperationTimeoutMs, r as createProviderOperationDeadline } from "./shared-CplO4ABs.mjs";
import "./response-limit-runtime-EJhmMies.mjs";
import { t as isProviderApiKeyConfigured } from "./provider-auth-availability-Def9yjI2.mjs";
import "./ssrf-runtime-DDaKW8yK.mjs";
import "./media-generation-runtime-CRQCGm3P.mjs";
import "./provider-http-fLggMdVs.mjs";
import { i as imageFileExtensionForMimeType, u as toImageDataUrl } from "./image-assets-0f0B2Wnj.mjs";
import "./image-generation-DbTnsmLw.mjs";
import "./provider-auth-DgA3mFGd.mjs";
import { t as resolveFalHttpRequestConfig } from "./http-config-DKBrGVe1.mjs";
//#region extensions/fal/image-generation-provider.ts
const DEFAULT_FAL_IMAGE_MODEL = "fal-ai/flux/dev";
const DEFAULT_FAL_EDIT_SUBPATH = "image-to-image";
const FAL_KREA_2_MODEL_PREFIX = "krea/v2/";
const FAL_KREA_2_MEDIUM_MODEL = "krea/v2/medium/text-to-image";
const FAL_KREA_2_LARGE_MODEL = "krea/v2/large/text-to-image";
const FAL_NANO_BANANA_MODEL = "fal-ai/nano-banana";
const FAL_NANO_BANANA_2_LITE_MODEL = "google/nano-banana-2-lite";
const FAL_GROK_IMAGINE_MODEL = "xai/grok-imagine-image";
const FAL_GPT_IMAGE_25_MODELS = [
"openai/gpt-image-2.5/flare/text-to-image",
"openai/gpt-image-2.5/flare/edit",
"openai/gpt-image-2.5/sunburst/text-to-image",
"openai/gpt-image-2.5/sunburst/edit"
];
const GPT_IMAGE_25_EDIT_MAX_INPUT_IMAGES = 16;
const GPT_IMAGE_25_QUALITIES = [
"low",
"medium",
"high",
"xhigh",
"max",
"auto"
];
const GPT_IMAGE_25_OUTPUT_FORMATS = [
"png",
"jpeg",
"webp"
];
const GPT_IMAGE_25_BACKGROUNDS = [
"transparent",
"opaque",
"auto"
];
const DEFAULT_OUTPUT_FORMAT = "png";
const GPT_IMAGE_EDIT_MAX_INPUT_IMAGES = 10;
const NANO_BANANA_LEGACY_EDIT_MAX_INPUT_IMAGES = 3;
const NANO_BANANA_EDIT_MAX_INPUT_IMAGES = 14;
const GROK_IMAGINE_EDIT_MAX_INPUT_IMAGES = 3;
const KREA_STYLE_REFERENCE_MAX_INPUT_IMAGES = 10;
const FAL_OUTPUT_FORMATS = ["png", "jpeg"];
const FAL_SUPPORTED_SIZES = [
"1024x1024",
"1024x1536",
"1536x1024",
"1024x1792",
"1792x1024"
];
const FAL_SUPPORTED_ASPECT_RATIOS = [
"1:1",
"2:3",
"3:2",
"2.35:1",
"3:4",
"4:3",
"4:5",
"5:4",
"9:16",
"16:9",
"21:9",
"4:1",
"1:4",
"8:1",
"1:8"
];
const KREA_SUPPORTED_ASPECT_RATIOS = [
"1:1",
"4:3",
"3:2",
"16:9",
"2.35:1",
"4:5",
"2:3",
"9:16"
];
const NANO_BANANA_LEGACY_SUPPORTED_ASPECT_RATIOS = [
"21:9",
"16:9",
"3:2",
"4:3",
"5:4",
"1:1",
"4:5",
"3:4",
"2:3",
"9:16"
];
const NANO_BANANA_SUPPORTED_ASPECT_RATIOS = [
...NANO_BANANA_LEGACY_SUPPORTED_ASPECT_RATIOS,
"4:1",
"1:4",
"8:1",
"1:8"
];
const GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS = [
"2:1",
"20:9",
"19.5:9",
"16:9",
"4:3",
"3:2",
"1:1",
"2:3",
"3:4",
"9:16",
"9:19.5",
"9:20",
"1:2"
];
const GROK_IMAGINE_SUPPORTED_RESOLUTIONS = ["1K", "2K"];
const KREA_CREATIVITY_LEVELS = [
"raw",
"low",
"medium",
"high"
];
const FAL_IMAGE_MALFORMED_RESPONSE = "fal image generation response malformed";
const DEFAULT_HTTP_TIMEOUT_MS = 3e4;
function parseFalImageGenerationResponse(payload) {
if (!isRecord(payload)) throw new Error(FAL_IMAGE_MALFORMED_RESPONSE);
const rawImages = payload.images;
if (rawImages === void 0 || rawImages === null) return {
images: [],
prompt: normalizeOptionalString(payload.prompt)
};
if (!Array.isArray(rawImages)) throw new Error(FAL_IMAGE_MALFORMED_RESPONSE);
const images = [];
for (const entry of rawImages) {
if (!isRecord(entry)) throw new Error(FAL_IMAGE_MALFORMED_RESPONSE);
images.push(entry);
}
return {
images,
prompt: normalizeOptionalString(payload.prompt)
};
}
function ensureFalModelPath(model, hasInputImages) {
const trimmed = model?.trim() || DEFAULT_FAL_IMAGE_MODEL;
const schema = resolveFalImageModelSchema(trimmed);
if (!hasInputImages || schema.appendEditPath === false) return trimmed;
if (isFalGptImage25Model(trimmed)) return trimmed.replace(/\/text-to-image$/, "/edit");
if (trimmed.endsWith(`/${schema.appendEditPath}`) || trimmed.endsWith("/edit") || trimmed.endsWith(`/${DEFAULT_FAL_EDIT_SUBPATH}`) || trimmed.includes("/image-to-image/")) return trimmed;
return `${trimmed}/${schema.appendEditPath}`;
}
function isFalGptImage25Model(model) {
return FAL_GPT_IMAGE_25_MODELS.some((candidate) => candidate === model);
}
function resolveFalImageModelSchema(model) {
const editDefaults = {
referenceImages: "image_urls",
referenceLimitNoun: "reference image",
appendEditPath: "edit",
supportsCount: true,
supportsOutputFormat: true
};
if (isFalGptImage25Model(model)) return {
...editDefaults,
geometry: "image_size",
maxInputImages: GPT_IMAGE_25_EDIT_MAX_INPUT_IMAGES,
referenceLimitLabel: "fal GPT Image 2.5 edit"
};
if (model.startsWith(FAL_KREA_2_MODEL_PREFIX)) return {
geometry: "native_aspect_ratio",
aspectRatios: KREA_SUPPORTED_ASPECT_RATIOS,
referenceImages: "image_style_references",
maxInputImages: KREA_STYLE_REFERENCE_MAX_INPUT_IMAGES,
referenceLimitLabel: "fal Krea 2",
referenceLimitNoun: "style reference",
appendEditPath: false,
supportsCount: false,
supportsOutputFormat: false
};
if (model === FAL_NANO_BANANA_MODEL || model.startsWith(`${FAL_NANO_BANANA_MODEL}/`)) return {
...editDefaults,
geometry: "native_aspect_ratio",
aspectRatios: NANO_BANANA_LEGACY_SUPPORTED_ASPECT_RATIOS,
resolutions: [],
maxInputImages: NANO_BANANA_LEGACY_EDIT_MAX_INPUT_IMAGES,
referenceLimitLabel: "fal Nano Banana"
};
if (model.startsWith("openai/gpt-image-") || model.startsWith(`${FAL_NANO_BANANA_MODEL}-`)) {
const isNanoBanana = model.startsWith(`${FAL_NANO_BANANA_MODEL}-`);
return {
...editDefaults,
geometry: isNanoBanana ? "native_aspect_ratio" : "image_size",
...isNanoBanana ? { aspectRatios: NANO_BANANA_SUPPORTED_ASPECT_RATIOS } : {},
maxInputImages: isNanoBanana ? NANO_BANANA_EDIT_MAX_INPUT_IMAGES : GPT_IMAGE_EDIT_MAX_INPUT_IMAGES,
referenceLimitLabel: isNanoBanana ? "fal Nano Banana 2" : "fal GPT Image edit"
};
}
if (model.startsWith(FAL_NANO_BANANA_2_LITE_MODEL)) return {
...editDefaults,
geometry: "native_aspect_ratio",
aspectRatios: NANO_BANANA_SUPPORTED_ASPECT_RATIOS,
resolutions: [],
maxInputImages: NANO_BANANA_EDIT_MAX_INPUT_IMAGES,
referenceLimitLabel: "fal Nano Banana 2 Lite"
};
if (model.startsWith(FAL_GROK_IMAGINE_MODEL)) return {
...editDefaults,
geometry: "native_aspect_ratio",
aspectRatios: GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS,
resolutions: GROK_IMAGINE_SUPPORTED_RESOLUTIONS,
resolutionCase: "lower",
maxInputImages: GROK_IMAGINE_EDIT_MAX_INPUT_IMAGES,
referenceLimitLabel: "fal Grok Imagine"
};
return {
geometry: "image_size",
referenceImages: "image_url",
maxInputImages: 1,
referenceLimitLabel: "fal flux image generation currently",
referenceLimitNoun: "reference image",
appendEditPath: "image-to-image",
supportsCount: true,
supportsOutputFormat: true
};
}
function parseSize(raw) {
const trimmed = raw?.trim();
if (!trimmed) return null;
const match = /^(\d{2,5})x(\d{2,5})$/iu.exec(trimmed);
if (!match) return null;
const width = Number.parseInt(match[1] ?? "", 10);
const height = Number.parseInt(match[2] ?? "", 10);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
return {
width,
height
};
}
function mapResolutionToEdge(resolution) {
if (!resolution) return;
return resolution === "4K" ? 4096 : resolution === "2K" ? 2048 : 1024;
}
const FAL_ASPECT_RATIO_SIZES = /* @__PURE__ */ new Map([
["1:1", "square_hd"],
["4:3", "landscape_4_3"],
["3:4", "portrait_4_3"],
["16:9", "landscape_16_9"],
["9:16", "portrait_16_9"]
]);
function parseAspectRatioParts(aspectRatio) {
const match = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/u.exec(aspectRatio.trim());
if (!match) throw new Error(`Invalid fal aspect ratio: ${aspectRatio}`);
const widthRatio = Number.parseFloat(match[1] ?? "");
const heightRatio = Number.parseFloat(match[2] ?? "");
if (!Number.isFinite(widthRatio) || !Number.isFinite(heightRatio) || widthRatio <= 0 || heightRatio <= 0) throw new Error(`Invalid fal aspect ratio: ${aspectRatio}`);
return {
widthRatio,
heightRatio
};
}
function aspectRatioToDimensions(aspectRatio, edge) {
const { widthRatio, heightRatio } = parseAspectRatioParts(aspectRatio);
if (widthRatio >= heightRatio) return {
width: edge,
height: Math.max(1, Math.round(edge * heightRatio / widthRatio))
};
return {
width: Math.max(1, Math.round(edge * widthRatio / heightRatio)),
height: edge
};
}
function resolveFalImageSize(params) {
const parsed = parseSize(params.size);
if (parsed) return parsed;
const normalizedAspectRatio = params.aspectRatio?.trim();
const edge = mapResolutionToEdge(params.resolution);
if (normalizedAspectRatio) {
if (edge && !params.hasInputImages) return aspectRatioToDimensions(normalizedAspectRatio, edge);
return FAL_ASPECT_RATIO_SIZES.get(normalizedAspectRatio) ?? aspectRatioToDimensions(normalizedAspectRatio, 1024);
}
return edge ? {
width: edge,
height: edge
} : void 0;
}
function resolveFalGptImage25AspectRatioSize(aspectRatio) {
const { width, height } = aspectRatioToDimensions(aspectRatio, 1536);
return {
width: Math.round(width / 16) * 16,
height: Math.round(height / 16) * 16
};
}
function validateFalGptImage25Size(size) {
if (size === void 0 || typeof size === "string") return;
const { width, height } = size;
const pixels = width * height;
if (width % 16 !== 0 || height % 16 !== 0 || Math.max(width, height) > 3840 || pixels < 655360 || pixels > 8294400 || width > height * 3 || height > width * 3) throw new Error("fal GPT Image 2.5 requires size dimensions divisible by 16, edges up to 3840, 655360-8294400 pixels, and aspect ratio between 1:3 and 3:1. Use size 1024x1024, 1536x1024, 1024x1536, or auto instead of incompatible geometry hints.");
}
function aspectRatioScore(aspectRatio, targetRatio) {
const { widthRatio, heightRatio } = parseAspectRatioParts(aspectRatio);
return Math.abs(Math.log(widthRatio / heightRatio) - Math.log(targetRatio));
}
function resolveClosestFalAspectRatioForSize(imageSize, aspectRatios) {
if (!imageSize || typeof imageSize === "string") return;
const targetRatio = imageSize.width / imageSize.height;
return aspectRatios.reduce((best, candidate) => {
if (!best) return candidate;
return aspectRatioScore(candidate, targetRatio) < aspectRatioScore(best, targetRatio) ? candidate : best;
}, void 0);
}
function resolveKreaCreativity(raw) {
const normalized = normalizeLowercaseStringOrEmpty(raw);
return KREA_CREATIVITY_LEVELS.includes(normalized) ? normalized : "medium";
}
function resolveFalCreativityOption(providerOptions) {
const falOptions = isRecord(providerOptions?.fal) ? providerOptions.fal : void 0;
return typeof falOptions?.creativity === "string" ? falOptions.creativity : "";
}
function resolveNativeFalAspectRatio(params) {
const requestedAspectRatio = params.aspectRatio?.trim();
const allowedAspectRatios = params.schema.aspectRatios;
if (requestedAspectRatio) {
if (allowedAspectRatios && !allowedAspectRatios.includes(requestedAspectRatio)) throw new Error(`${params.schema.referenceLimitLabel} supports aspectRatio values: ${allowedAspectRatios.join(", ")}`);
return requestedAspectRatio;
}
if (allowedAspectRatios) return resolveClosestFalAspectRatioForSize(params.imageSize, allowedAspectRatios);
}
function applyFalImageGeometry(params) {
if (params.schema.geometry === "native_aspect_ratio") {
if (params.resolution && params.schema.referenceImages === "image_style_references") throw new Error("fal Krea 2 supports aspectRatio but not resolution overrides");
const nativeAspectRatio = resolveNativeFalAspectRatio({
schema: params.schema,
aspectRatio: params.aspectRatio,
imageSize: params.size ? params.imageSize : void 0
});
if (nativeAspectRatio) params.requestBody.aspect_ratio = nativeAspectRatio;
if (params.resolution && params.schema.referenceImages === "image_urls") {
const allowedResolutions = params.schema.resolutions;
if (allowedResolutions === void 0) params.requestBody.resolution = params.resolution;
else if (allowedResolutions.length === 0) throw new Error(`${params.schema.referenceLimitLabel} does not support resolution overrides`);
else if (!allowedResolutions.includes(params.resolution)) throw new Error(`${params.schema.referenceLimitLabel} supports resolution values: ${allowedResolutions.join(", ")}`);
else params.requestBody.resolution = params.schema.resolutionCase === "lower" ? params.resolution.toLowerCase() : params.resolution;
}
return;
}
if (params.imageSize !== void 0) params.requestBody.image_size = params.imageSize;
}
function applyFalReferenceImages(params) {
const encoded = params.inputImages.map((img) => toImageDataUrl(img));
if (params.schema.referenceImages === "image_urls") {
params.requestBody.image_urls = encoded;
return;
}
if (params.schema.referenceImages === "image_style_references") {
params.requestBody.image_style_references = encoded.map((imageUrl) => ({ image_url: imageUrl }));
return;
}
const [input] = encoded;
if (!input) throw new Error("fal image edit request missing reference image");
params.requestBody.image_url = input;
}
function formatFalReferenceLimitError(schema, inputImageCount) {
const limit = schema.maxInputImages === 1 ? "one" : String(schema.maxInputImages);
const noun = schema.maxInputImages === 1 ? schema.referenceLimitNoun : `${schema.referenceLimitNoun}s`;
return `${schema.referenceLimitLabel} supports at most ${limit} ${noun} (requested ${inputImageCount})`;
}
async function fetchImageBuffer(url, deadline, maxBytes) {
const { response, release } = await fetchWithSsrFGuard({
url,
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: deadline.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS
}),
auditContext: "fal-image-download"
});
try {
await assertOkOrThrowProviderError(response, "fal image download failed");
const mimeType = response.headers.get("content-type")?.trim() || "image/png";
return {
buffer: await readResponseWithLimit(response, maxBytes, { onOverflow: ({ maxBytes: maxBytesLocal }) => /* @__PURE__ */ new Error(`fal generated image download exceeds ${maxBytesLocal} bytes`) }),
mimeType
};
} finally {
await release();
}
}
function buildFalImageGenerationProvider() {
return {
id: "fal",
label: "fal",
defaultModel: DEFAULT_FAL_IMAGE_MODEL,
models: [
DEFAULT_FAL_IMAGE_MODEL,
`${DEFAULT_FAL_IMAGE_MODEL}/${DEFAULT_FAL_EDIT_SUBPATH}`,
FAL_KREA_2_MEDIUM_MODEL,
FAL_KREA_2_LARGE_MODEL,
...FAL_GPT_IMAGE_25_MODELS
],
isConfigured: (ctx) => isProviderApiKeyConfigured({
provider: "fal",
...ctx
}),
capabilities: {
generate: {
maxCount: 4,
supportsSize: true,
supportsAspectRatio: true,
supportsResolution: true
},
edit: {
enabled: true,
maxCount: 4,
maxInputImages: 1,
maxInputImagesByModel: {
...Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, GPT_IMAGE_25_EDIT_MAX_INPUT_IMAGES])),
[FAL_NANO_BANANA_MODEL]: NANO_BANANA_LEGACY_EDIT_MAX_INPUT_IMAGES,
[`${FAL_NANO_BANANA_MODEL}/edit`]: NANO_BANANA_LEGACY_EDIT_MAX_INPUT_IMAGES
},
maxInputImagesByModelPrefix: {
"openai/gpt-image-": GPT_IMAGE_EDIT_MAX_INPUT_IMAGES,
[FAL_KREA_2_MODEL_PREFIX]: KREA_STYLE_REFERENCE_MAX_INPUT_IMAGES,
[`${FAL_NANO_BANANA_MODEL}-`]: NANO_BANANA_EDIT_MAX_INPUT_IMAGES,
[FAL_NANO_BANANA_2_LITE_MODEL]: NANO_BANANA_EDIT_MAX_INPUT_IMAGES,
[FAL_GROK_IMAGINE_MODEL]: GROK_IMAGINE_EDIT_MAX_INPUT_IMAGES
},
supportsSize: true,
supportsAspectRatio: true,
supportsResolution: true
},
geometry: {
sizes: [...FAL_SUPPORTED_SIZES],
sizesByModel: {
...Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, []])),
[FAL_KREA_2_MEDIUM_MODEL]: [],
[FAL_KREA_2_LARGE_MODEL]: []
},
aspectRatios: [...FAL_SUPPORTED_ASPECT_RATIOS],
aspectRatiosByModel: Object.fromEntries([
FAL_NANO_BANANA_MODEL,
`${FAL_NANO_BANANA_MODEL}/edit`,
FAL_NANO_BANANA_2_LITE_MODEL,
`${FAL_NANO_BANANA_2_LITE_MODEL}/edit`,
FAL_GROK_IMAGINE_MODEL,
`${FAL_GROK_IMAGINE_MODEL}/edit`,
`${FAL_GROK_IMAGINE_MODEL}/quality`,
`${FAL_GROK_IMAGINE_MODEL}/quality/edit`,
FAL_KREA_2_MEDIUM_MODEL,
FAL_KREA_2_LARGE_MODEL,
`${FAL_NANO_BANANA_MODEL}-2`,
`${FAL_NANO_BANANA_MODEL}-2/edit`
].flatMap((model) => {
const aspectRatios = resolveFalImageModelSchema(model).aspectRatios;
return aspectRatios ? [[model, [...aspectRatios]]] : [];
})),
resolutions: [
"1K",
"2K",
"4K"
],
resolutionsByModel: {
...Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, []])),
[FAL_KREA_2_MEDIUM_MODEL]: [],
[FAL_KREA_2_LARGE_MODEL]: [],
[FAL_NANO_BANANA_MODEL]: [],
[`${FAL_NANO_BANANA_MODEL}/edit`]: [],
[FAL_NANO_BANANA_2_LITE_MODEL]: [],
[`${FAL_NANO_BANANA_2_LITE_MODEL}/edit`]: [],
[FAL_GROK_IMAGINE_MODEL]: [...GROK_IMAGINE_SUPPORTED_RESOLUTIONS],
[`${FAL_GROK_IMAGINE_MODEL}/edit`]: [...GROK_IMAGINE_SUPPORTED_RESOLUTIONS],
[`${FAL_GROK_IMAGINE_MODEL}/quality`]: [...GROK_IMAGINE_SUPPORTED_RESOLUTIONS],
[`${FAL_GROK_IMAGINE_MODEL}/quality/edit`]: [...GROK_IMAGINE_SUPPORTED_RESOLUTIONS]
}
},
output: {
formats: [...FAL_OUTPUT_FORMATS],
formatsByModel: Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, [...GPT_IMAGE_25_OUTPUT_FORMATS]])),
qualitiesByModel: Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, [...GPT_IMAGE_25_QUALITIES]])),
backgroundsByModel: Object.fromEntries(FAL_GPT_IMAGE_25_MODELS.map((model) => [model, [...GPT_IMAGE_25_BACKGROUNDS]]))
}
},
async generateImage(req) {
const deadline = createProviderOperationDeadline({
timeoutMs: req.timeoutMs,
label: "fal image generation"
});
const inputImageCount = req.inputImages?.length ?? 0;
const hasInputImages = inputImageCount > 0;
const requestedModel = req.model?.trim() || DEFAULT_FAL_IMAGE_MODEL;
const schema = resolveFalImageModelSchema(requestedModel);
const isGptImage25 = isFalGptImage25Model(requestedModel);
if (isGptImage25 && req.resolution) throw new Error("fal GPT Image 2.5 does not support resolution overrides; use size instead");
if (isGptImage25 && req.size && req.size !== "auto" && !parseSize(req.size)) throw new Error("fal GPT Image 2.5 size must be WIDTHxHEIGHT or auto");
const imageSize = isGptImage25 && req.size === "auto" ? "auto" : isGptImage25 && req.aspectRatio && !req.size ? resolveFalGptImage25AspectRatioSize(req.aspectRatio) : resolveFalImageSize({
size: req.size,
resolution: req.resolution,
aspectRatio: req.aspectRatio,
hasInputImages
});
if (isGptImage25) validateFalGptImage25Size(imageSize);
const model = ensureFalModelPath(req.model, hasInputImages);
if (hasInputImages && inputImageCount > schema.maxInputImages) throw new Error(formatFalReferenceLimitError(schema, inputImageCount));
if (hasInputImages && schema.referenceImages === "image_url") {
if (req.aspectRatio) throw new Error("fal flux image edit endpoint does not support aspectRatio overrides");
}
if (!schema.supportsCount && (req.count ?? 1) > 1) throw new Error(`fal ${requestedModel} supports one output image per request`);
if (!schema.supportsOutputFormat && req.outputFormat) throw new Error(`fal ${requestedModel} does not support outputFormat overrides`);
const { baseUrl, headers, dispatcherPolicy } = await resolveFalHttpRequestConfig({
req,
capability: "image"
});
const maxImageBytes = resolveGeneratedMediaMaxBytes(req.cfg, "image");
const requestBody = {
prompt: req.prompt,
...schema.supportsCount ? { num_images: req.count ?? 1 } : {},
...schema.supportsOutputFormat ? { output_format: req.outputFormat ?? DEFAULT_OUTPUT_FORMAT } : {},
...isGptImage25 && req.quality ? { quality: req.quality } : {},
...isGptImage25 && req.background ? { background: req.background } : {}
};
if (schema.referenceImages === "image_style_references") requestBody.creativity = resolveKreaCreativity(resolveFalCreativityOption(req.providerOptions));
applyFalImageGeometry({
requestBody,
schema,
imageSize,
size: req.size,
aspectRatio: req.aspectRatio,
resolution: req.resolution
});
if (hasInputImages) applyFalReferenceImages({
requestBody,
schema,
inputImages: req.inputImages ?? []
});
const { response, release } = await fetchWithSsrFGuard({
url: `${baseUrl}/${model}`,
init: {
method: "POST",
headers,
body: JSON.stringify(requestBody)
},
timeoutMs: deadline.timeoutMs === void 0 ? void 0 : resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: deadline.timeoutMs
}),
dispatcherPolicy,
auditContext: "fal-image-generate"
});
try {
await assertOkOrThrowHttpError(response, "fal image generation failed");
const payload = parseFalImageGenerationResponse(await readProviderJsonResponse(response, "fal.image-generation"));
const images = [];
let imageIndex = 0;
for (const entry of payload.images) {
const url = normalizeOptionalString(entry.url);
if (!url) throw new Error(FAL_IMAGE_MALFORMED_RESPONSE);
const downloaded = await fetchImageBuffer(url, deadline, maxImageBytes);
imageIndex += 1;
images.push({
buffer: downloaded.buffer,
mimeType: downloaded.mimeType,
fileName: `image-${imageIndex}.${imageFileExtensionForMimeType(downloaded.mimeType || normalizeOptionalString(entry.content_type))}`
});
}
if (images.length === 0) throw new Error("fal image generation response missing image data");
return {
images,
model,
metadata: payload.prompt ? { prompt: payload.prompt } : void 0
};
} finally {
await release();
}
}
};
}
//#endregion
export { buildFalImageGenerationProvider as t };