Files
9router/open-sse/handlers/imageProviders/cloudflareAi.js
T
decoluaandCursor e53ce79abb refactor(open-sse): B1 consolidate media endpoint URLs into registry
- image/embed/tts/search base URLs derive from media.*Config.baseUrl /
  searchViaChat.endpoint (single source); handlers read PROVIDER_MEDIA.
- ~18 hardcoded endpoints moved: bfl/fal/stability/runway/hf/gemini/
  cloudflare/recraft/sdwebui/comfyui/nanobanana image, voyage embed,
  gemini/openrouter tts, chatSearch endpoints.
- Byte-identical: PROVIDERS 62 + alias 90 baselines, image buildUrl outputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 13:58:12 +07:00

180 lines
4.9 KiB
JavaScript

import { nowSec, urlToBase64 } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = PROVIDER_MEDIA["cloudflare-ai"]?.imageConfig?.baseUrl;
const MULTIPART_MODELS = new Set([
"@cf/black-forest-labs/flux-2-dev",
"@cf/black-forest-labs/flux-2-klein-4b",
"@cf/black-forest-labs/flux-2-klein-9b",
]);
const OPTIONAL_FIELDS = [
"negative_prompt",
"guidance",
"seed",
"num_steps",
"steps",
"strength",
];
function sizeToDimensions(size) {
const match = /^(\d+)x(\d+)$/.exec(String(size || ""));
if (!match) return {};
return {
width: Number(match[1]),
height: Number(match[2]),
};
}
function getDimensions(body) {
return {
...sizeToDimensions(body.size),
...(Number.isFinite(Number(body.width)) ? { width: Number(body.width) } : {}),
...(Number.isFinite(Number(body.height)) ? { height: Number(body.height) } : {}),
};
}
async function resolveImageInput(value) {
if (Array.isArray(value)) {
return { bytes: value, b64: Buffer.from(value).toString("base64") };
}
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
if (/^https?:\/\//i.test(trimmed)) {
const b64 = await urlToBase64(trimmed);
return { bytes: base64ToBytes(b64), b64 };
}
const match = /^data:image\/[^;]+;base64,(.+)$/i.exec(trimmed);
const b64 = match ? match[1] : trimmed;
return { bytes: base64ToBytes(b64), b64 };
}
function base64ToBytes(value) {
try {
return Array.from(Buffer.from(value, "base64"));
} catch {
return value;
}
}
function addOptionalFields(target, body, append) {
for (const key of OPTIONAL_FIELDS) {
const value = body[key];
if (value === undefined || value === null || value === "") continue;
append(target, key, value);
}
}
async function buildJsonBody(body) {
const req = { prompt: body.prompt, ...getDimensions(body) };
addOptionalFields(req, body, (target, key, value) => {
target[key] = value;
});
const imageData = await resolveImageInput(body.image);
if (imageData) {
req.image_b64 = imageData.b64;
req.image = imageData.bytes;
}
const maskData = await resolveImageInput(body.mask_image || body.maskImage || body.mask);
if (maskData) {
req.mask_b64 = maskData.b64;
req.mask = maskData.bytes;
req.mask_image = maskData.bytes;
}
return req;
}
function buildMultipartBody(body) {
const form = new FormData();
form.append("prompt", body.prompt);
const dimensions = getDimensions(body);
for (const [key, value] of Object.entries(dimensions)) {
form.append(key, String(value));
}
addOptionalFields(form, body, (target, key, value) => {
target.append(key, String(value));
});
return form;
}
function imageItemFromString(value) {
if (typeof value !== "string" || !value) return null;
if (/^data:image\/[^;]+;base64,/i.test(value)) {
return { b64_json: value.replace(/^data:image\/[^;]+;base64,/i, "") };
}
if (/^https?:\/\//i.test(value)) return { url: value };
return { b64_json: value };
}
function normalizeCloudflareResponse(responseBody) {
if (responseBody?.created && Array.isArray(responseBody?.data)) return responseBody;
const result = responseBody?.result ?? responseBody;
const queuedResponse = Array.isArray(result?.responses)
? result.responses.find((item) => item?.success !== false)?.result
: null;
if (queuedResponse) return normalizeCloudflareResponse(queuedResponse);
const image =
(typeof result === "string" ? result : null) ||
result?.image ||
result?.data?.[0]?.b64_json ||
result?.data?.[0]?.url;
const item = imageItemFromString(image);
return {
created: nowSec(),
data: item ? [item] : [],
};
}
export default {
buildUrl: (model, creds) => {
const accountId = creds?.providerSpecificData?.accountId;
if (!accountId) throw new Error("cloudflare-ai requires accountId in providerSpecificData");
return `${BASE_URL}/${accountId}/ai/run/${model}`;
},
buildHeaders: (creds, requestBody) => {
const headers = {};
const isMultipart = typeof FormData !== "undefined" && requestBody instanceof FormData;
if (!isMultipart) {
headers["Content-Type"] = "application/json";
}
const key = creds?.apiKey || creds?.accessToken;
if (key) headers.Authorization = `Bearer ${key}`;
return headers;
},
buildBody: async (model, body) => (
MULTIPART_MODELS.has(model)
? buildMultipartBody(body)
: await buildJsonBody(body)
),
async parseResponse(response) {
const contentType = (response.headers.get("Content-Type") || "").toLowerCase();
if (contentType.startsWith("image/")) {
const buf = await response.arrayBuffer();
return {
created: nowSec(),
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
};
}
const json = await response.json();
return normalizeCloudflareResponse(json);
},
normalize: normalizeCloudflareResponse,
};