Skip to content

Commit 51de1e0

Browse files
committed
refactor: modularize imageGeneration.ts into focused modules (#3594)
1 parent e2522e2 commit 51de1e0

25 files changed

Lines changed: 4328 additions & 3776 deletions

open-sse/handlers/imageGeneration.ts

Lines changed: 1 addition & 3776 deletions
Large diffs are not rendered by default.
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
2+
/**
3+
* Image Generation Handler
4+
*
5+
* Handles POST /v1/images/generations requests.
6+
* Proxies to upstream image generation providers using OpenAI-compatible format.
7+
*
8+
* Request format (OpenAI-compatible):
9+
* {
10+
* "model": "openai/gpt-image-2",
11+
* "prompt": "a beautiful sunset over mountains",
12+
* "n": 1,
13+
* "size": "1024x1024",
14+
* "quality": "standard", // optional: "standard" | "hd"
15+
* "response_format": "url" // optional: "url" | "b64_json"
16+
* }
17+
*/
18+
19+
import { getImageProvider, parseImageModel } from "../../config/imageRegistry.ts";
20+
21+
import { mapImageSize } from "../../translator/image/sizeMapper.ts";
22+
23+
import { sleep } from "../../utils/sleep.ts";
24+
25+
import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../../utils/error.ts";
26+
27+
import { extractImageInputs, normalizeRequestedImageFormat, resolveImageSource, isHttpUrl, parseSizeToDimensions, normalizeProviderImagePayload, normalizePositiveNumber } from "./utils.ts";
28+
import { saveImageErrorResult, saveImageSuccessResult } from "./logging.ts";
29+
30+
31+
32+
const BFL_MODEL_ENDPOINTS = {
33+
"flux-2-max": "/v1/flux-2-max",
34+
"flux-2-pro": "/v1/flux-2-pro",
35+
"flux-2-flex": "/v1/flux-2-flex",
36+
"flux-2-klein-9b": "/v1/flux-2-klein-9b",
37+
"flux-2-klein-4b": "/v1/flux-2-klein-4b",
38+
"flux-kontext-pro": "/v1/flux-kontext-pro",
39+
"flux-kontext-max": "/v1/flux-kontext-max",
40+
"flux-pro-1.1": "/v1/flux-pro-1.1",
41+
"flux-pro-1.1-ultra": "/v1/flux-pro-1.1-ultra",
42+
"flux-dev": "/v1/flux-dev",
43+
"flux-pro": "/v1/flux-pro",
44+
};
45+
46+
47+
48+
const BFL_EDIT_MODELS = new Set([
49+
"flux-2-max",
50+
"flux-2-pro",
51+
"flux-2-flex",
52+
"flux-kontext-pro",
53+
"flux-kontext-max",
54+
]);
55+
56+
57+
58+
const BFL_FAILURE_STATUSES = new Set(["Error", "Failed", "Content Moderated", "Request Moderated"]);
59+
60+
61+
62+
export async function handleBlackForestLabsImageGeneration({
63+
model,
64+
provider,
65+
providerConfig,
66+
body,
67+
credentials,
68+
log,
69+
}) {
70+
const startTime = Date.now();
71+
const token = credentials?.apiKey || credentials?.accessToken;
72+
const endpoint = BFL_MODEL_ENDPOINTS[model];
73+
74+
if (!endpoint) {
75+
return {
76+
success: false,
77+
status: 400,
78+
error: `Unsupported Black Forest Labs image model: ${model}`,
79+
};
80+
}
81+
82+
const { imageUrl, maskUrl } = extractImageInputs(body);
83+
const upstreamBody: Record<string, unknown> = {
84+
prompt: body.prompt,
85+
output_format: normalizeRequestedImageFormat(body, "png"),
86+
};
87+
88+
try {
89+
if (BFL_EDIT_MODELS.has(model) && imageUrl) {
90+
upstreamBody.input_image = (await resolveImageSource(imageUrl)).base64;
91+
} else if (imageUrl && isHttpUrl(imageUrl)) {
92+
upstreamBody.image_url = imageUrl;
93+
}
94+
95+
if (maskUrl && (model === "flux-pro-1.0-fill" || model === "flux-kontext-pro")) {
96+
upstreamBody.mask = (await resolveImageSource(maskUrl)).base64;
97+
}
98+
99+
if (model === "flux-kontext-pro" || model === "flux-kontext-max") {
100+
upstreamBody.aspect_ratio = body.aspect_ratio || mapImageSize(body.size);
101+
} else if (typeof body.size === "string" && body.size.includes("x")) {
102+
const { width, height } = parseSizeToDimensions(body.size, 1024);
103+
upstreamBody.width = width;
104+
upstreamBody.height = height;
105+
}
106+
107+
if (body.seed !== undefined) upstreamBody.seed = body.seed;
108+
if (body.n !== undefined && model.includes("ultra"))
109+
upstreamBody.num_images = Number(body.n) || 1;
110+
if (body.quality === "hd" && model.includes("ultra")) upstreamBody.raw = true;
111+
if (body.left !== undefined) upstreamBody.left = body.left;
112+
if (body.right !== undefined) upstreamBody.right = body.right;
113+
if (body.top !== undefined) upstreamBody.top = body.top;
114+
if (body.bottom !== undefined) upstreamBody.bottom = body.bottom;
115+
if (body.steps !== undefined) upstreamBody.steps = body.steps;
116+
if (body.guidance !== undefined) upstreamBody.guidance = body.guidance;
117+
if (body.grow_mask !== undefined) upstreamBody.grow_mask = body.grow_mask;
118+
if (body.safety_tolerance !== undefined) upstreamBody.safety_tolerance = body.safety_tolerance;
119+
120+
if (log) {
121+
const promptPreview = String(body.prompt ?? "").slice(0, 60);
122+
log.info("IMAGE", `${provider}/${model} (black-forest-labs) | prompt: "${promptPreview}..."`);
123+
}
124+
125+
const response = await fetch(`${providerConfig.baseUrl.replace(/\/$/, "")}${endpoint}`, {
126+
method: "POST",
127+
headers: {
128+
"Content-Type": "application/json",
129+
Accept: "application/json",
130+
"x-key": token,
131+
},
132+
body: JSON.stringify(upstreamBody),
133+
});
134+
135+
if (!response.ok) {
136+
const errorText = await response.text();
137+
if (log)
138+
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
139+
return saveImageErrorResult({
140+
provider,
141+
model,
142+
status: response.status,
143+
startTime,
144+
error: errorText,
145+
requestBody: upstreamBody,
146+
});
147+
}
148+
149+
const initialPayload = await response.json();
150+
const finalPayload = initialPayload?.polling_url
151+
? await pollBlackForestLabsResult({
152+
pollingUrl: initialPayload?.polling_url,
153+
token,
154+
body,
155+
log,
156+
})
157+
: initialPayload;
158+
159+
const images = await normalizeProviderImagePayload(finalPayload, body, log);
160+
return saveImageSuccessResult({
161+
provider,
162+
model,
163+
startTime,
164+
requestBody: upstreamBody,
165+
responseBody: { images_count: images.length },
166+
created: finalPayload.created,
167+
images,
168+
});
169+
} catch (err) {
170+
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
171+
return saveImageErrorResult({
172+
provider,
173+
model,
174+
status: 502,
175+
startTime,
176+
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
177+
});
178+
}
179+
}
180+
181+
182+
183+
async function pollBlackForestLabsResult({ pollingUrl, token, body, log }) {
184+
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 300000);
185+
const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, 1500);
186+
const deadline = Date.now() + timeoutMs;
187+
188+
while (Date.now() < deadline) {
189+
const response = await fetch(pollingUrl, {
190+
method: "GET",
191+
headers: {
192+
"x-key": token,
193+
},
194+
});
195+
196+
if (!response.ok) {
197+
const errorText = await response.text();
198+
throw new Error(`BFL polling failed (${response.status}): ${errorText}`);
199+
}
200+
201+
const payload = await response.json();
202+
const status = payload?.status;
203+
204+
if (status === "Ready") {
205+
return payload;
206+
}
207+
208+
if (BFL_FAILURE_STATUSES.has(status)) {
209+
throw new Error(`BFL image generation failed: ${status}`);
210+
}
211+
212+
if (log) {
213+
log.info("IMAGE", `black-forest-labs polling status: ${String(status || "Pending")}`);
214+
}
215+
216+
await sleep(pollIntervalMs);
217+
}
218+
219+
throw new Error(`BFL polling timed out after ${timeoutMs}ms`);
220+
}
221+

0 commit comments

Comments
 (0)