mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
refactor(imageGeneration): extract 8 provider families to co-located files (#4609)
Integrated into release/v3.8.34 (extraction completed: added missing imports/exports per module, main imports handlers locally; 145 image-gen tests pass, typecheck/cycles/file-size green)
This commit is contained in:
@@ -44,6 +44,24 @@ import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
|
||||
import { FetchTimeoutError, fetchWithTimeout, getConfiguredTimeout } from "@/shared/utils/fetchTimeout";
|
||||
import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts";
|
||||
|
||||
// --- Per-provider handlers (extracted to co-located files in PR-#4582-batch) ---
|
||||
// Imported locally so internal callers (handleImageGeneration / handleImageEdit)
|
||||
// resolve to a real binding. extractMarkdownImageUrls + CHATGPT_WEB_IMAGE_ID_RE
|
||||
// are still used by handleImageEdit below, so they are imported (not re-defined).
|
||||
import { handleSDWebUIImageGeneration } from "./imageGeneration/providers/sdWebUI.ts";
|
||||
import { handleHyperbolicImageGeneration } from "./imageGeneration/providers/hyperbolic.ts";
|
||||
import { handleComfyUIImageGeneration } from "./imageGeneration/providers/comfyUI.ts";
|
||||
import { handleImagen3ImageGeneration } from "./imageGeneration/providers/imagen3.ts";
|
||||
import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideogram.ts";
|
||||
import { handleHaiperImageGeneration } from "./imageGeneration/providers/haiper.ts";
|
||||
import { handleLeonardoImageGeneration } from "./imageGeneration/providers/leonardo.ts";
|
||||
import {
|
||||
handleChatGptWebImageGeneration,
|
||||
extractMarkdownImageUrls,
|
||||
CHATGPT_WEB_IMAGE_ID_RE,
|
||||
} from "./imageGeneration/providers/chatgptWeb.ts";
|
||||
|
||||
|
||||
interface KieImageOptions {
|
||||
model: string;
|
||||
provider: string;
|
||||
@@ -1096,190 +1114,6 @@ export async function handleOpenAIImageEdit({
|
||||
return result;
|
||||
}
|
||||
|
||||
const CHATGPT_WEB_IMAGE_MARKDOWN_RE = /!\[[^\]]*\]\(([^)\s]+)\)/g;
|
||||
const CHATGPT_WEB_IMAGE_ID_RE = /\/v1\/chatgpt-web\/image\/([a-f0-9]{16,64})(?=[?\s"'<>)]|$)/i;
|
||||
|
||||
function extractMarkdownImageUrls(text: string): string[] {
|
||||
const urls: string[] = [];
|
||||
// String.prototype.matchAll consumes a fresh iterator and ignores the
|
||||
// regex's lastIndex, so no manual reset is required.
|
||||
for (const match of text.matchAll(CHATGPT_WEB_IMAGE_MARKDOWN_RE)) {
|
||||
if (match[1]) urls.push(match[1]);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function buildChatGptWebImagePrompt(body): string {
|
||||
const prompt = String(body.prompt || "").trim();
|
||||
const details: string[] = [`Create an image for this prompt: ${prompt}`];
|
||||
if (typeof body.size === "string" && body.size.trim()) {
|
||||
details.push(`Requested size: ${body.size.trim()}.`);
|
||||
}
|
||||
if (typeof body.quality === "string" && body.quality.trim()) {
|
||||
details.push(`Requested quality: ${body.quality.trim()}.`);
|
||||
}
|
||||
if (typeof body.style === "string" && body.style.trim()) {
|
||||
details.push(`Requested style: ${body.style.trim()}.`);
|
||||
}
|
||||
return details.join("\n");
|
||||
}
|
||||
|
||||
async function handleChatGptWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
clientHeaders,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: "Prompt is required for ChatGPT Web image generation",
|
||||
});
|
||||
}
|
||||
|
||||
if (!credentials?.apiKey) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "ChatGPT Web credentials missing session cookie",
|
||||
});
|
||||
}
|
||||
|
||||
// Each image is one chatgpt.com chat turn (~30s). Cap at 4 (matches OpenAI's
|
||||
// own limit for GPT Image models) so a stray n=1000 doesn't pin the
|
||||
// executor for hours before the upstream HTTP timeout fires.
|
||||
const CHATGPT_WEB_IMAGE_N_MAX = 4;
|
||||
const rawCount = Number.isInteger(body.n) && (body.n as number) > 0 ? (body.n as number) : 1;
|
||||
if (rawCount > CHATGPT_WEB_IMAGE_N_MAX) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: `ChatGPT Web image generation supports n=1..${CHATGPT_WEB_IMAGE_N_MAX} (got ${rawCount}); each n is a separate ~30s chat turn.`,
|
||||
});
|
||||
}
|
||||
const requestedCount = rawCount;
|
||||
if (log && requestedCount > 1) {
|
||||
log.warn(
|
||||
"IMAGE",
|
||||
`ChatGPT Web returns one image per chat turn; requested n=${requestedCount} will run sequentially`
|
||||
);
|
||||
}
|
||||
|
||||
const wantsBase64 = body.response_format === "b64_json";
|
||||
const images: Array<{ url?: string; b64_json?: string }> = [];
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: prompt.slice(0, 500),
|
||||
size: body.size || undefined,
|
||||
quality: body.quality || undefined,
|
||||
};
|
||||
|
||||
for (let i = 0; i < requestedCount; i++) {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model,
|
||||
body: {
|
||||
messages: [{ role: "user", content: buildChatGptWebImagePrompt(body) }],
|
||||
},
|
||||
stream: false,
|
||||
credentials,
|
||||
signal,
|
||||
log,
|
||||
clientHeaders,
|
||||
});
|
||||
|
||||
const responseText = await result.response.text();
|
||||
if (result.response.status >= 400) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: result.response.status,
|
||||
startTime,
|
||||
error: responseText,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
const json = JSON.parse(responseText);
|
||||
content = String(json?.choices?.[0]?.message?.content || "");
|
||||
} catch {
|
||||
content = responseText;
|
||||
}
|
||||
|
||||
const urls = extractMarkdownImageUrls(content);
|
||||
if (urls.length === 0) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `ChatGPT Web completed without returning image markdown: ${content.slice(0, 300)}`,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
if (!wantsBase64) {
|
||||
images.push({ url });
|
||||
continue;
|
||||
}
|
||||
const id = url.match(CHATGPT_WEB_IMAGE_ID_RE)?.[1];
|
||||
const cached = id ? getChatGptImage(id) : null;
|
||||
if (!cached) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: "ChatGPT Web image bytes expired before b64_json conversion",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
images.push({ b64_json: cached.bytes.toString("base64") });
|
||||
}
|
||||
}
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody,
|
||||
responseBody: { images_count: images.length },
|
||||
images,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a multipart /v1/images/edits request for chatgpt-web. Open WebUI
|
||||
* uploads the prior image's bytes; we hash them and look up our cache.
|
||||
*
|
||||
* The hash match is reliable because Open WebUI's image-gen pipeline
|
||||
* downloads our /v1/chatgpt-web/image/<id> URL byte-for-byte and re-serves
|
||||
* those exact bytes through its own file store. When the user asks to edit
|
||||
* the image, OWUI uploads the same bytes back to us via multipart — same
|
||||
* hash, we find the conversation context, and drive the executor with a
|
||||
* synthetic chat thread that triggers continuation mode.
|
||||
*
|
||||
* No-match cases (cache evicted by TTL, or the user uploaded a foreign
|
||||
* image) get a clear 400. We can't actually edit an image we don't have a
|
||||
* conversation context for — chatgpt.com's image_gen tool needs the
|
||||
* original conversation node, and we don't have a path to upload bytes
|
||||
* directly.
|
||||
*/
|
||||
export async function handleImageEdit({
|
||||
provider,
|
||||
model,
|
||||
@@ -2538,7 +2372,7 @@ async function handleCodexImageGeneration({
|
||||
});
|
||||
}
|
||||
|
||||
function saveImageSuccessResult({
|
||||
export function saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
@@ -2567,7 +2401,7 @@ function saveImageSuccessResult({
|
||||
};
|
||||
}
|
||||
|
||||
function saveImageErrorResult({ provider, model, status, startTime, error, requestBody = null }) {
|
||||
export function saveImageErrorResult({ provider, model, status, startTime, error, requestBody = null }) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
@@ -2658,104 +2492,6 @@ async function fetchImageEndpoint(url, headers, body, provider, log) {
|
||||
* Handle Hyperbolic image generation
|
||||
* Uses { model_name, prompt, height, width } and returns { images: [{ image: base64 }] }
|
||||
*/
|
||||
async function handleHyperbolicImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
|
||||
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
|
||||
|
||||
const upstreamBody = {
|
||||
model_name: model,
|
||||
prompt: body.prompt,
|
||||
height: height || 1024,
|
||||
width: width || 1024,
|
||||
backend: "auto",
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (hyperbolic) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Transform { images: [{ image: base64 }] } → OpenAI format
|
||||
const images = (data.images || []).map((img) => ({
|
||||
b64_json: img.image,
|
||||
revised_prompt: body.prompt,
|
||||
}));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle NanoBanana image generation
|
||||
* NanoBanana is async (submit task -> poll status -> return final image URL/base64)
|
||||
*/
|
||||
async function handleNanoBananaImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
@@ -3117,660 +2853,4 @@ function normalizePositiveNumber(value, fallback) {
|
||||
* Handle SD WebUI image generation (local, no auth)
|
||||
* POST {baseUrl} with { prompt, negative_prompt, width, height, steps }
|
||||
* Response: { images: ["base64..."] }
|
||||
*/
|
||||
async function handleSDWebUIImageGeneration({ model, provider, providerConfig, body, log }) {
|
||||
const startTime = Date.now();
|
||||
const [width, height] = (body.size || "512x512").split("x").map(Number);
|
||||
|
||||
const upstreamBody = {
|
||||
prompt: body.prompt,
|
||||
negative_prompt: body.negative_prompt || "",
|
||||
width: width || 512,
|
||||
height: height || 512,
|
||||
steps: body.steps || 20,
|
||||
cfg_scale: body.cfg_scale || 7,
|
||||
sampler_name: body.sampler || "Euler a",
|
||||
batch_size: body.n || 1,
|
||||
override_settings: {
|
||||
sd_model_checkpoint: model,
|
||||
},
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (sdwebui) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// SD WebUI returns { images: ["base64...", ...] }
|
||||
const images = (data.images || []).map((b64) => ({
|
||||
b64_json: b64,
|
||||
revised_prompt: body.prompt,
|
||||
}));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} sdwebui error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ComfyUI image generation (local, no auth)
|
||||
* Submits a txt2img workflow, polls for completion, fetches output
|
||||
*/
|
||||
async function handleComfyUIImageGeneration({ model, provider, providerConfig, body, log }) {
|
||||
const startTime = Date.now();
|
||||
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
|
||||
|
||||
// Default txt2img workflow template for ComfyUI
|
||||
const workflow = {
|
||||
"3": {
|
||||
class_type: "KSampler",
|
||||
inputs: {
|
||||
seed: parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % 2 ** 32,
|
||||
steps: body.steps || 20,
|
||||
cfg: body.cfg_scale || 7,
|
||||
sampler_name: "euler",
|
||||
scheduler: "normal",
|
||||
denoise: 1,
|
||||
model: ["4", 0],
|
||||
positive: ["6", 0],
|
||||
negative: ["7", 0],
|
||||
latent_image: ["5", 0],
|
||||
},
|
||||
},
|
||||
"4": {
|
||||
class_type: "CheckpointLoaderSimple",
|
||||
inputs: { ckpt_name: model },
|
||||
},
|
||||
"5": {
|
||||
class_type: "EmptyLatentImage",
|
||||
inputs: { width: width || 1024, height: height || 1024, batch_size: body.n || 1 },
|
||||
},
|
||||
"6": {
|
||||
class_type: "CLIPTextEncode",
|
||||
inputs: { text: body.prompt, clip: ["4", 1] },
|
||||
},
|
||||
"7": {
|
||||
class_type: "CLIPTextEncode",
|
||||
inputs: { text: body.negative_prompt || "", clip: ["4", 1] },
|
||||
},
|
||||
"8": {
|
||||
class_type: "VAEDecode",
|
||||
inputs: { samples: ["3", 0], vae: ["4", 2] },
|
||||
},
|
||||
"9": {
|
||||
class_type: "SaveImage",
|
||||
inputs: { filename_prefix: "omniroute", images: ["8", 0] },
|
||||
},
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const promptId = await submitComfyWorkflow(providerConfig.baseUrl, workflow);
|
||||
const historyEntry = await pollComfyResult(providerConfig.baseUrl, promptId);
|
||||
const outputFiles = extractComfyOutputFiles(historyEntry);
|
||||
|
||||
const images = [];
|
||||
for (const file of outputFiles) {
|
||||
const buffer = await fetchComfyOutput(
|
||||
providerConfig.baseUrl,
|
||||
file.filename,
|
||||
file.subfolder,
|
||||
file.type
|
||||
);
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
images.push({ b64_json: base64, revised_prompt: body.prompt });
|
||||
}
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} comfyui error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHaiperImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (haiper) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", HAIPER_KEY: token },
|
||||
body: JSON.stringify({ prompt, aspect_ratio: body.aspect_ratio || "16:9" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { job_id } = await res.json();
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.statusUrl}/${job_id}`, {
|
||||
headers: { HAIPER_KEY: token },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
if (status.status === "completed" || status.status === "succeeded") {
|
||||
const imgUrl = status.creation_url || status.output?.image_url;
|
||||
if (imgUrl) {
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (status.status === "failed") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Haiper image generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Haiper image generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Haiper image generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Haiper image generation timed out" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} haiper error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLeonardoImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (leonardo) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
modelId: model || "phoenix",
|
||||
prompt,
|
||||
width: body.width || 1024,
|
||||
height: body.height || 1024,
|
||||
num_images: 1,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { sdGenerationJob } = await res.json();
|
||||
const genId = sdGenerationJob?.generationId;
|
||||
if (!genId) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No generation ID returned",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No generation ID returned" };
|
||||
}
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
const gen = status.generations_by_pk || status;
|
||||
if (gen.status === "COMPLETE") {
|
||||
const imgUrl = gen.generated_images?.[0]?.url;
|
||||
if (imgUrl) {
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (gen.status === "FAILED") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo image generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Leonardo image generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo image generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Leonardo image generation timed out" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} leonardo error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIdeogramImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (ideogram) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Api-Key": token },
|
||||
body: JSON.stringify({ prompt, aspect_ratio: "ASPECT_16_9", model: model || "V_3" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.data && data.data.length > 0) {
|
||||
const imgUrl = data.data[0].url;
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No images returned from Ideogram",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No images returned from Ideogram" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} ideogram error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type Imagen3ImageGenArgs = {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: { baseUrl: string };
|
||||
body: { prompt?: string; size?: string; n?: number };
|
||||
credentials: { apiKey?: string; accessToken?: string };
|
||||
log?: {
|
||||
info?: (tag: string, msg: string) => void;
|
||||
error?: (tag: string, msg: string) => void;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type Imagen3NormalizedImage = {
|
||||
b64_json?: unknown;
|
||||
url?: unknown;
|
||||
revised_prompt?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Imagen 3 image generation
|
||||
*/
|
||||
async function handleImagen3ImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}: Imagen3ImageGenArgs) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
const aspectRatio = mapImageSize(body.size);
|
||||
|
||||
const upstreamBody = {
|
||||
prompt: body.prompt,
|
||||
aspect_ratio: aspectRatio,
|
||||
number_of_images: body.n ?? 1,
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info(
|
||||
"IMAGE",
|
||||
`${provider}/${model} (imagen3) | prompt: "${promptPreview}..." | aspect_ratio: ${aspectRatio}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
requestBody: upstreamBody,
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Normalize response to OpenAI format
|
||||
const images: Imagen3NormalizedImage[] = [];
|
||||
if (Array.isArray(data.images)) {
|
||||
images.push(
|
||||
...data.images.map((img: Record<string, unknown>) => ({
|
||||
b64_json: img.image ?? img.b64_json ?? img.url ?? img,
|
||||
revised_prompt: body.prompt,
|
||||
}))
|
||||
);
|
||||
} else if (Array.isArray(data.data)) {
|
||||
images.push(...data.data);
|
||||
} else if (data.url || data.b64_json || data.image) {
|
||||
images.push({
|
||||
b64_json: data.image || data.b64_json || data.url,
|
||||
url: data.url,
|
||||
revised_prompt: body.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: data.created || Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (log) log.error("IMAGE", `${provider} fetch error: ${errMsg}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errMsg,
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: 502, error: `Image provider error: ${errMsg}` };
|
||||
}
|
||||
}
|
||||
*/
|
||||
175
open-sse/handlers/imageGeneration/providers/chatgptWeb.ts
Normal file
175
open-sse/handlers/imageGeneration/providers/chatgptWeb.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: chatgpt-web | Module: chatgptWeb | Lines: 1102-1282 (181 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { ChatGptWebExecutor } from "../../../executors/chatgpt-web.ts";
|
||||
import { getChatGptImage } from "../../../services/chatgptImageCache.ts";
|
||||
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
|
||||
|
||||
export const CHATGPT_WEB_IMAGE_MARKDOWN_RE = /!\[[^\]]*\]\(([^)\s]+)\)/g;
|
||||
export const CHATGPT_WEB_IMAGE_ID_RE =
|
||||
/\/v1\/chatgpt-web\/image\/([a-f0-9]{16,64})(?=[?\s"'<>)]|$)/i;
|
||||
|
||||
export function extractMarkdownImageUrls(text: string): string[] {
|
||||
const urls: string[] = [];
|
||||
// String.prototype.matchAll consumes a fresh iterator and ignores the
|
||||
// regex's lastIndex, so no manual reset is required.
|
||||
for (const match of text.matchAll(CHATGPT_WEB_IMAGE_MARKDOWN_RE)) {
|
||||
if (match[1]) urls.push(match[1]);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export function buildChatGptWebImagePrompt(body): string {
|
||||
const prompt = String(body.prompt || "").trim();
|
||||
const details: string[] = [`Create an image for this prompt: ${prompt}`];
|
||||
if (typeof body.size === "string" && body.size.trim()) {
|
||||
details.push(`Requested size: ${body.size.trim()}.`);
|
||||
}
|
||||
if (typeof body.quality === "string" && body.quality.trim()) {
|
||||
details.push(`Requested quality: ${body.quality.trim()}.`);
|
||||
}
|
||||
if (typeof body.style === "string" && body.style.trim()) {
|
||||
details.push(`Requested style: ${body.style.trim()}.`);
|
||||
}
|
||||
return details.join("\n");
|
||||
}
|
||||
|
||||
export async function handleChatGptWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
clientHeaders,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: "Prompt is required for ChatGPT Web image generation",
|
||||
});
|
||||
}
|
||||
|
||||
if (!credentials?.apiKey) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "ChatGPT Web credentials missing session cookie",
|
||||
});
|
||||
}
|
||||
|
||||
// Each image is one chatgpt.com chat turn (~30s). Cap at 4 (matches OpenAI's
|
||||
// own limit for GPT Image models) so a stray n=1000 doesn't pin the
|
||||
// executor for hours before the upstream HTTP timeout fires.
|
||||
const CHATGPT_WEB_IMAGE_N_MAX = 4;
|
||||
const rawCount = Number.isInteger(body.n) && (body.n as number) > 0 ? (body.n as number) : 1;
|
||||
if (rawCount > CHATGPT_WEB_IMAGE_N_MAX) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: `ChatGPT Web image generation supports n=1..${CHATGPT_WEB_IMAGE_N_MAX} (got ${rawCount}); each n is a separate ~30s chat turn.`,
|
||||
});
|
||||
}
|
||||
const requestedCount = rawCount;
|
||||
if (log && requestedCount > 1) {
|
||||
log.warn(
|
||||
"IMAGE",
|
||||
`ChatGPT Web returns one image per chat turn; requested n=${requestedCount} will run sequentially`
|
||||
);
|
||||
}
|
||||
|
||||
const wantsBase64 = body.response_format === "b64_json";
|
||||
const images: Array<{ url?: string; b64_json?: string }> = [];
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: prompt.slice(0, 500),
|
||||
size: body.size || undefined,
|
||||
quality: body.quality || undefined,
|
||||
};
|
||||
|
||||
for (let i = 0; i < requestedCount; i++) {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model,
|
||||
body: {
|
||||
messages: [{ role: "user", content: buildChatGptWebImagePrompt(body) }],
|
||||
},
|
||||
stream: false,
|
||||
credentials,
|
||||
signal,
|
||||
log,
|
||||
clientHeaders,
|
||||
});
|
||||
|
||||
const responseText = await result.response.text();
|
||||
if (result.response.status >= 400) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: result.response.status,
|
||||
startTime,
|
||||
error: responseText,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
const json = JSON.parse(responseText);
|
||||
content = String(json?.choices?.[0]?.message?.content || "");
|
||||
} catch {
|
||||
content = responseText;
|
||||
}
|
||||
|
||||
const urls = extractMarkdownImageUrls(content);
|
||||
if (urls.length === 0) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `ChatGPT Web completed without returning image markdown: ${content.slice(0, 300)}`,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
if (!wantsBase64) {
|
||||
images.push({ url });
|
||||
continue;
|
||||
}
|
||||
const id = url.match(CHATGPT_WEB_IMAGE_ID_RE)?.[1];
|
||||
const cached = id ? getChatGptImage(id) : null;
|
||||
if (!cached) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: "ChatGPT Web image bytes expired before b64_json conversion",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
images.push({ b64_json: cached.bytes.toString("base64") });
|
||||
}
|
||||
}
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody,
|
||||
responseBody: { images_count: images.length },
|
||||
images,
|
||||
});
|
||||
}
|
||||
116
open-sse/handlers/imageGeneration/providers/comfyUI.ts
Normal file
116
open-sse/handlers/imageGeneration/providers/comfyUI.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: comfyui | Module: comfyUI | Lines: 3213-3314 (102 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { randomUUID } from "crypto";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
import {
|
||||
submitComfyWorkflow,
|
||||
pollComfyResult,
|
||||
fetchComfyOutput,
|
||||
extractComfyOutputFiles,
|
||||
} from "../../../utils/comfyuiClient.ts";
|
||||
|
||||
export async function handleComfyUIImageGeneration({ model, provider, providerConfig, body, log }) {
|
||||
const startTime = Date.now();
|
||||
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
|
||||
|
||||
// Default txt2img workflow template for ComfyUI
|
||||
const workflow = {
|
||||
"3": {
|
||||
class_type: "KSampler",
|
||||
inputs: {
|
||||
seed: parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % 2 ** 32,
|
||||
steps: body.steps || 20,
|
||||
cfg: body.cfg_scale || 7,
|
||||
sampler_name: "euler",
|
||||
scheduler: "normal",
|
||||
denoise: 1,
|
||||
model: ["4", 0],
|
||||
positive: ["6", 0],
|
||||
negative: ["7", 0],
|
||||
latent_image: ["5", 0],
|
||||
},
|
||||
},
|
||||
"4": {
|
||||
class_type: "CheckpointLoaderSimple",
|
||||
inputs: { ckpt_name: model },
|
||||
},
|
||||
"5": {
|
||||
class_type: "EmptyLatentImage",
|
||||
inputs: { width: width || 1024, height: height || 1024, batch_size: body.n || 1 },
|
||||
},
|
||||
"6": {
|
||||
class_type: "CLIPTextEncode",
|
||||
inputs: { text: body.prompt, clip: ["4", 1] },
|
||||
},
|
||||
"7": {
|
||||
class_type: "CLIPTextEncode",
|
||||
inputs: { text: body.negative_prompt || "", clip: ["4", 1] },
|
||||
},
|
||||
"8": {
|
||||
class_type: "VAEDecode",
|
||||
inputs: { samples: ["3", 0], vae: ["4", 2] },
|
||||
},
|
||||
"9": {
|
||||
class_type: "SaveImage",
|
||||
inputs: { filename_prefix: "omniroute", images: ["8", 0] },
|
||||
},
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const promptId = await submitComfyWorkflow(providerConfig.baseUrl, workflow);
|
||||
const historyEntry = await pollComfyResult(providerConfig.baseUrl, promptId);
|
||||
const outputFiles = extractComfyOutputFiles(historyEntry);
|
||||
|
||||
const images = [];
|
||||
for (const file of outputFiles) {
|
||||
const buffer = await fetchComfyOutput(
|
||||
providerConfig.baseUrl,
|
||||
file.filename,
|
||||
file.subfolder,
|
||||
file.type
|
||||
);
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
images.push({ b64_json: base64, revised_prompt: body.prompt });
|
||||
}
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} comfyui error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
120
open-sse/handlers/imageGeneration/providers/haiper.ts
Normal file
120
open-sse/handlers/imageGeneration/providers/haiper.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: haiper | Module: haiper | Lines: 3315-3426 (112 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sleep } from "../../../utils/sleep.ts";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
export async function handleHaiperImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (haiper) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", HAIPER_KEY: token },
|
||||
body: JSON.stringify({ prompt, aspect_ratio: body.aspect_ratio || "16:9" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { job_id } = await res.json();
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.statusUrl}/${job_id}`, {
|
||||
headers: { HAIPER_KEY: token },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
if (status.status === "completed" || status.status === "succeeded") {
|
||||
const imgUrl = status.creation_url || status.output?.image_url;
|
||||
if (imgUrl) {
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (status.status === "failed") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Haiper image generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Haiper image generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Haiper image generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Haiper image generation timed out" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} haiper error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
100
open-sse/handlers/imageGeneration/providers/hyperbolic.ts
Normal file
100
open-sse/handlers/imageGeneration/providers/hyperbolic.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: hyperbolic | Module: hyperbolic | Lines: 2661-2758 (98 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
export async function handleHyperbolicImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
|
||||
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
|
||||
|
||||
const upstreamBody = {
|
||||
model_name: model,
|
||||
prompt: body.prompt,
|
||||
height: height || 1024,
|
||||
width: width || 1024,
|
||||
backend: "auto",
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (hyperbolic) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Transform { images: [{ image: base64 }] } → OpenAI format
|
||||
const images = (data.images || []).map((img) => ({
|
||||
b64_json: img.image,
|
||||
revised_prompt: body.prompt,
|
||||
}));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
96
open-sse/handlers/imageGeneration/providers/ideogram.ts
Normal file
96
open-sse/handlers/imageGeneration/providers/ideogram.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: ideogram | Module: ideogram | Lines: 3559-3669 (111 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
export async function handleIdeogramImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (ideogram) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Api-Key": token },
|
||||
body: JSON.stringify({ prompt, aspect_ratio: "ASPECT_16_9", model: model || "V_3" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.data && data.data.length > 0) {
|
||||
const imgUrl = data.data[0].url;
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No images returned from Ideogram",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No images returned from Ideogram" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} ideogram error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
136
open-sse/handlers/imageGeneration/providers/imagen3.ts
Normal file
136
open-sse/handlers/imageGeneration/providers/imagen3.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: imagen3 | Module: imagen3 | Lines: 3670-3777 (108 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { mapImageSize } from "../../../translator/image/sizeMapper.ts";
|
||||
|
||||
type Imagen3ImageGenArgs = {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: { baseUrl: string };
|
||||
body: { prompt?: string; size?: string; n?: number };
|
||||
credentials: { apiKey?: string; accessToken?: string };
|
||||
log?: {
|
||||
info?: (tag: string, msg: string) => void;
|
||||
error?: (tag: string, msg: string) => void;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type Imagen3NormalizedImage = {
|
||||
b64_json?: unknown;
|
||||
url?: unknown;
|
||||
revised_prompt?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Imagen 3 image generation
|
||||
*/
|
||||
export async function handleImagen3ImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}: Imagen3ImageGenArgs) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
const aspectRatio = mapImageSize(body.size);
|
||||
|
||||
const upstreamBody = {
|
||||
prompt: body.prompt,
|
||||
aspect_ratio: aspectRatio,
|
||||
number_of_images: body.n ?? 1,
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info(
|
||||
"IMAGE",
|
||||
`${provider}/${model} (imagen3) | prompt: "${promptPreview}..." | aspect_ratio: ${aspectRatio}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
requestBody: upstreamBody,
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Normalize response to OpenAI format
|
||||
const images: Imagen3NormalizedImage[] = [];
|
||||
if (Array.isArray(data.images)) {
|
||||
images.push(
|
||||
...data.images.map((img: Record<string, unknown>) => ({
|
||||
b64_json: img.image ?? img.b64_json ?? img.url ?? img,
|
||||
revised_prompt: body.prompt,
|
||||
}))
|
||||
);
|
||||
} else if (Array.isArray(data.data)) {
|
||||
images.push(...data.data);
|
||||
} else if (data.url || data.b64_json || data.image) {
|
||||
images.push({
|
||||
b64_json: data.image || data.b64_json || data.url,
|
||||
url: data.url,
|
||||
revised_prompt: body.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: data.created || Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (log) log.error("IMAGE", `${provider} fetch error: ${errMsg}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errMsg,
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: 502, error: `Image provider error: ${errMsg}` };
|
||||
}
|
||||
}
|
||||
|
||||
140
open-sse/handlers/imageGeneration/providers/leonardo.ts
Normal file
140
open-sse/handlers/imageGeneration/providers/leonardo.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: leonardo | Module: leonardo | Lines: 3427-3558 (132 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sleep } from "../../../utils/sleep.ts";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
export async function handleLeonardoImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
if (log) {
|
||||
log.info("IMAGE", `${provider}/${model} (leonardo) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
modelId: model || "phoenix",
|
||||
prompt,
|
||||
width: body.width || 1024,
|
||||
height: body.height || 1024,
|
||||
num_images: 1,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { sdGenerationJob } = await res.json();
|
||||
const genId = sdGenerationJob?.generationId;
|
||||
if (!genId) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No generation ID returned",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No generation ID returned" };
|
||||
}
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
const gen = status.generations_by_pk || status;
|
||||
if (gen.status === "COMPLETE") {
|
||||
const imgUrl = gen.generated_images?.[0]?.url;
|
||||
if (imgUrl) {
|
||||
const imgRes = await fetch(imgUrl);
|
||||
if (!imgRes.ok) {
|
||||
return {
|
||||
success: false,
|
||||
status: imgRes.status,
|
||||
error: `Failed to download image: ${imgRes.status}`,
|
||||
};
|
||||
}
|
||||
const buf = await imgRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (gen.status === "FAILED") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo image generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Leonardo image generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo image generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Leonardo image generation timed out" };
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} leonardo error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
94
open-sse/handlers/imageGeneration/providers/sdWebUI.ts
Normal file
94
open-sse/handlers/imageGeneration/providers/sdWebUI.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: sd-webui | Module: sdWebUI | Lines: 3121-3212 (92 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
export async function handleSDWebUIImageGeneration({ model, provider, providerConfig, body, log }) {
|
||||
const startTime = Date.now();
|
||||
const [width, height] = (body.size || "512x512").split("x").map(Number);
|
||||
|
||||
const upstreamBody = {
|
||||
prompt: body.prompt,
|
||||
negative_prompt: body.negative_prompt || "",
|
||||
width: width || 512,
|
||||
height: height || 512,
|
||||
steps: body.steps || 20,
|
||||
cfg_scale: body.cfg_scale || 7,
|
||||
sampler_name: body.sampler || "Euler a",
|
||||
batch_size: body.n || 1,
|
||||
override_settings: {
|
||||
sd_model_checkpoint: model,
|
||||
},
|
||||
};
|
||||
|
||||
if (log) {
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info("IMAGE", `${provider}/${model} (sdwebui) | prompt: "${promptPreview}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(upstreamBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (log)
|
||||
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: response.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: response.status, error: errorText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// SD WebUI returns { images: ["base64...", ...] }
|
||||
const images = (data.images || []).map((b64) => ({
|
||||
b64_json: b64,
|
||||
revised_prompt: body.prompt,
|
||||
}));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { images_count: images.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: images },
|
||||
};
|
||||
} catch (err) {
|
||||
if (log) log.error("IMAGE", `${provider} sdwebui error: ${err.message}`);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/images/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user