openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
279 lines (278 loc) • 12.2 kB
JavaScript
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js";
import { t as canonicalizeBase64 } from "./base64-l6yrCyHc.js";
import { u as sanitizeConfiguredModelProviderRequest } from "./provider-request-config-0Zg8QIAI.js";
import { r as assertOkOrThrowHttpError } from "./provider-http-errors-DqaqQLLZ.js";
import { c as postJsonRequest, l as postMultipartRequest, m as resolveProviderOperationTimeoutMs, n as createProviderOperationDeadline, p as resolveProviderHttpRequestConfig } from "./shared-IuCmx8oC.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { r as isProviderApiKeyConfigured } from "./provider-auth-DAOC_qI9.js";
import { a as resolveApiKeyForProvider } from "./provider-auth-runtime-D2xYnWtb.js";
import "./provider-http-BXCBCi4E.js";
//#region src/image-generation/image-assets.ts
/** Converts image provider base64/data-url payloads into generated or source image assets. */
const DEFAULT_IMAGE_MIME_TYPE = "image/png";
const DEFAULT_IMAGE_FILE_PREFIX = "image";
function throwMalformedImageResponse(message) {
if (message) throw new Error(message);
}
/** Maps an image MIME type to a stable filename extension. */
function imageFileExtensionForMimeType(mimeType, fallback = "png") {
const normalized = normalizeOptionalLowercaseString(mimeType)?.split(";")[0]?.trim();
if (!normalized) return fallback;
if (normalized.includes("jpeg") || normalized.includes("jpg")) return "jpg";
if (normalized.includes("svg")) return "svg";
const slashIndex = normalized.indexOf("/");
return slashIndex >= 0 ? normalized.slice(slashIndex + 1) || fallback : fallback;
}
function sniffImageMimeType(buffer, fallbackMimeType = DEFAULT_IMAGE_MIME_TYPE) {
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) return {
mimeType: "image/jpeg",
extension: "jpg"
};
if (buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) return {
mimeType: "image/png",
extension: "png"
};
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") return {
mimeType: "image/webp",
extension: "webp"
};
return {
mimeType: fallbackMimeType,
extension: imageFileExtensionForMimeType(fallbackMimeType)
};
}
function toImageDataUrl(params) {
return `data:${normalizeOptionalString(params.mimeType) ?? normalizeOptionalString(params.defaultMimeType) ?? DEFAULT_IMAGE_MIME_TYPE};base64,${params.buffer.toString("base64")}`;
}
function parseImageDataUrl(dataUrl) {
const match = dataUrl.match(/^data:(image\/[^;,]+)(?:;[^,]*)?;base64,(.+)$/is);
if (!match) return;
const mimeType = normalizeOptionalString(match[1]);
const base64 = normalizeOptionalString(match[2]);
if (!mimeType || !base64) return;
const canonicalBase64 = canonicalizeBase64(base64);
if (!canonicalBase64) return;
return {
mimeType,
base64: canonicalBase64
};
}
function generatedImageAssetFromBase64(params) {
const base64 = normalizeOptionalString(params.base64);
const canonicalBase64 = base64 ? canonicalizeBase64(base64) : void 0;
if (!canonicalBase64) return;
const buffer = Buffer.from(canonicalBase64, "base64");
const explicitMimeType = normalizeOptionalString(params.mimeType);
const defaultMimeType = normalizeOptionalString(params.defaultMimeType) ?? DEFAULT_IMAGE_MIME_TYPE;
const detected = params.sniffMimeType && !explicitMimeType ? sniffImageMimeType(buffer, defaultMimeType) : void 0;
const mimeType = explicitMimeType ?? detected?.mimeType ?? defaultMimeType;
const image = {
buffer,
mimeType,
fileName: `${normalizeOptionalString(params.fileNamePrefix) ?? DEFAULT_IMAGE_FILE_PREFIX}-${params.index + 1}.${detected?.extension ?? imageFileExtensionForMimeType(mimeType)}`
};
const revisedPrompt = normalizeOptionalString(params.revisedPrompt);
if (revisedPrompt) image.revisedPrompt = revisedPrompt;
return image;
}
function generatedImageAssetFromDataUrl(params) {
const parsed = parseImageDataUrl(params.dataUrl);
if (!parsed) return;
return generatedImageAssetFromBase64({
base64: parsed.base64,
index: params.index,
mimeType: parsed.mimeType,
fileNamePrefix: params.fileNamePrefix
});
}
function generatedImageAssetFromOpenAiCompatibleEntry(entry, index, options = {}) {
return generatedImageAssetFromBase64({
base64: normalizeOptionalString(entry.b64_json),
index,
mimeType: normalizeOptionalString(entry.mime_type),
revisedPrompt: normalizeOptionalString(entry.revised_prompt),
defaultMimeType: options.defaultMimeType,
fileNamePrefix: options.fileNamePrefix,
sniffMimeType: options.sniffMimeType
});
}
function parseOpenAiCompatibleImageResponse(payload, options = {}) {
if (!isRecord(payload)) {
throwMalformedImageResponse(options.malformedResponseError);
return [];
}
const data = payload.data;
if (data === void 0 || data === null) return [];
if (!Array.isArray(data)) {
throwMalformedImageResponse(options.malformedResponseError);
return [];
}
const images = [];
for (const [index, entry] of data.entries()) {
if (!isRecord(entry)) {
throwMalformedImageResponse(options.malformedResponseError);
continue;
}
const image = generatedImageAssetFromOpenAiCompatibleEntry(entry, index, options);
if (!image) {
throwMalformedImageResponse(options.malformedResponseError);
continue;
}
images.push(image);
}
return images;
}
function imageSourceUploadFileName(params) {
const fileName = normalizeOptionalString(params.image.fileName);
if (fileName) return fileName;
const mimeType = normalizeOptionalString(params.image.mimeType) ?? normalizeOptionalString(params.defaultMimeType) ?? DEFAULT_IMAGE_MIME_TYPE;
return `${normalizeOptionalString(params.fileNamePrefix) ?? DEFAULT_IMAGE_FILE_PREFIX}-${params.index + 1}.${imageFileExtensionForMimeType(mimeType)}`;
}
//#endregion
//#region src/image-generation/openai-compatible-image-provider.ts
function readProviderConfig(cfg, providerConfigKey) {
return cfg?.models?.providers?.[providerConfigKey];
}
function resolveDefaultModel(model, fallback) {
return normalizeOptionalString(model) ?? fallback;
}
function trimTrailingSlash(value) {
return value.replace(/\/+$/u, "");
}
function appendImagesPath(baseUrl, mode) {
return `${trimTrailingSlash(baseUrl)}/images/${mode === "edit" ? "edits" : "generations"}`;
}
function resolveRequestTimeoutMs(params) {
if (params.options.defaultTimeoutMs === void 0) return params.req.timeoutMs;
const label = params.mode === "edit" ? params.options.failureLabels?.edit ?? `${params.options.label} image edit` : params.options.failureLabels?.generate ?? `${params.options.label} image generation`;
return resolveProviderOperationTimeoutMs({
deadline: createProviderOperationDeadline({
timeoutMs: params.req.timeoutMs,
label
}),
defaultTimeoutMs: params.options.defaultTimeoutMs
});
}
/** Creates an image-generation provider backed by OpenAI-style image endpoints. */
function createOpenAiCompatibleImageGenerationProvider(options) {
const providerConfigKey = options.providerConfigKey ?? options.id;
const normalizeModel = options.normalizeModel ?? resolveDefaultModel;
const resolveCount = options.resolveCount ?? (({ req }) => {
return req.count ?? 1;
});
return {
id: options.id,
label: options.label,
defaultModel: options.defaultModel,
...options.defaultTimeoutMs !== void 0 ? { defaultTimeoutMs: options.defaultTimeoutMs } : {},
models: [...options.models],
isConfigured: ({ agentDir }) => isProviderApiKeyConfigured({
provider: options.id,
agentDir
}),
capabilities: options.capabilities,
async generateImage(req) {
const inputImages = req.inputImages ?? [];
const mode = inputImages.length > 0 ? "edit" : "generate";
const maxInputImages = options.capabilities.edit.maxInputImages;
if (mode === "edit" && !options.capabilities.edit.enabled) throw new Error(`${options.label} image editing is not supported.`);
if (mode === "edit" && maxInputImages !== void 0 && inputImages.length > maxInputImages) throw new Error(options.tooManyInputImagesError ?? `${options.label} image editing supports up to ${maxInputImages} reference image${maxInputImages === 1 ? "" : "s"}.`);
if (mode === "edit" && inputImages.length === 0) throw new Error(options.missingInputImageError ?? `${options.label} image edit missing reference image.`);
const auth = await resolveApiKeyForProvider({
provider: options.id,
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore
});
if (!auth.apiKey) throw new Error(options.missingApiKeyError ?? `${options.label} API key missing`);
const providerConfig = readProviderConfig(req.cfg, providerConfigKey);
const resolvedBaseUrl = options.resolveBaseUrl?.({
req,
providerConfig,
defaultBaseUrl: options.defaultBaseUrl
}) ?? normalizeOptionalString(providerConfig?.baseUrl) ?? options.defaultBaseUrl;
const allowPrivateNetwork = options.resolveAllowPrivateNetwork?.({
baseUrl: resolvedBaseUrl,
req,
providerConfig
});
const { baseUrl, allowPrivateNetwork: resolvedAllowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({
baseUrl: resolvedBaseUrl,
defaultBaseUrl: options.defaultBaseUrl,
allowPrivateNetwork,
request: options.useConfiguredRequest ? sanitizeConfiguredModelProviderRequest(providerConfig?.request) : void 0,
defaultHeaders: { Authorization: `Bearer ${auth.apiKey}` },
provider: options.id,
capability: "image",
transport: "http"
});
const model = normalizeModel(req.model, options.defaultModel);
const requestParams = {
req,
inputImages,
model,
count: resolveCount({
req,
mode
}),
mode
};
const requestBody = mode === "edit" ? options.buildEditRequest({
...requestParams,
mode
}) : options.buildGenerateRequest({
...requestParams,
mode
});
const timeoutMs = resolveRequestTimeoutMs({
options,
req,
mode
});
const { response, release } = await (requestBody.kind === "multipart" ? postMultipartRequest({
url: appendImagesPath(baseUrl, mode),
headers: (() => {
const multipartHeaders = new Headers(headers);
multipartHeaders.delete("Content-Type");
return multipartHeaders;
})(),
body: requestBody.form,
timeoutMs,
fetchFn: fetch,
allowPrivateNetwork: resolvedAllowPrivateNetwork,
ssrfPolicy: req.ssrfPolicy,
dispatcherPolicy
}) : postJsonRequest({
url: appendImagesPath(baseUrl, mode),
headers: (() => {
const jsonHeaders = new Headers(headers);
jsonHeaders.set("Content-Type", "application/json");
return jsonHeaders;
})(),
body: requestBody.body,
timeoutMs,
fetchFn: fetch,
allowPrivateNetwork: resolvedAllowPrivateNetwork,
ssrfPolicy: req.ssrfPolicy,
dispatcherPolicy
}));
try {
await assertOkOrThrowHttpError(response, mode === "edit" ? options.failureLabels?.edit ?? `${options.label} image edit failed` : options.failureLabels?.generate ?? `${options.label} image generation failed`);
const images = parseOpenAiCompatibleImageResponse(await response.json(), {
...options.response,
malformedResponseError: mode === "edit" ? `${options.label} image edit response malformed` : `${options.label} image generation response malformed`
});
if (images.length === 0) throw new Error(options.emptyResponseError ?? (mode === "edit" ? `${options.label} image edit response missing image data` : `${options.label} image generation response missing image data`));
return {
images,
model
};
} finally {
await release();
}
}
};
}
//#endregion
export { imageFileExtensionForMimeType as a, parseOpenAiCompatibleImageResponse as c, generatedImageAssetFromOpenAiCompatibleEntry as i, sniffImageMimeType as l, generatedImageAssetFromBase64 as n, imageSourceUploadFileName as o, generatedImageAssetFromDataUrl as r, parseImageDataUrl as s, createOpenAiCompatibleImageGenerationProvider as t, toImageDataUrl as u };