File size: 6,528 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { getExecutor } from "../executors/index.js";
import { getImageAdapter } from "./imageProviders/index.js";
import { urlToBase64 } from "./imageProviders/_base.js";
function serializeRequestBody(requestBody) {
if (typeof FormData !== "undefined" && requestBody instanceof FormData) return requestBody;
if (typeof requestBody === "string") return requestBody;
return JSON.stringify(requestBody);
}
/**
* Core image generation handler — orchestrator only.
* Provider-specific URL/headers/body/parse/normalize live in `./imageProviders/{id}.js`.
*
* @param {object} options
* @param {object} options.body - Request body { model, prompt, n, size, ... }
* @param {object} options.modelInfo - { provider, model }
* @param {object} options.credentials - Provider credentials
* @param {object} [options.log] - Logger
* @param {boolean} [options.streamToClient] - Pipe SSE to client (codex)
* @param {boolean} [options.binaryOutput] - Return raw image bytes
* @param {function} [options.onCredentialsRefreshed]
* @param {function} [options.onRequestSuccess]
* @returns {Promise<{ success: boolean, response: Response, status?: number, error?: string }>}
*/
export async function handleImageGenerationCore({
body,
modelInfo,
credentials,
log,
streamToClient = false,
binaryOutput = false,
onCredentialsRefreshed,
onRequestSuccess,
}) {
const { provider, model } = modelInfo;
if (!body.prompt) {
return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: prompt");
}
const adapter = getImageAdapter(provider);
if (!adapter) {
return createErrorResult(
HTTP_STATUS.BAD_REQUEST,
`Provider '${provider}' does not support image generation`
);
}
let url;
let headers;
let requestBody;
try {
url = adapter.buildUrl(model, credentials);
requestBody = await adapter.buildBody(model, body);
headers = adapter.buildHeaders(credentials, requestBody, model, body);
} catch (error) {
return createErrorResult(HTTP_STATUS.BAD_REQUEST, error.message || `Invalid ${provider} image request`);
}
log?.debug?.("IMAGE", `${provider.toUpperCase()} | ${model} | prompt="${body.prompt.slice(0, 50)}..."`);
let providerResponse;
try {
providerResponse = await fetch(url, {
method: "POST",
headers,
body: serializeRequestBody(requestBody),
});
} catch (error) {
const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY);
log?.debug?.("IMAGE", `Fetch error: ${errMsg}`);
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg);
}
// Handle 401/403 — try token refresh (skipped for noAuth providers)
const executor = getExecutor(provider);
if (
!executor?.noAuth &&
!adapter.noAuth &&
(providerResponse.status === HTTP_STATUS.UNAUTHORIZED ||
providerResponse.status === HTTP_STATUS.FORBIDDEN)
) {
const newCredentials = await refreshWithRetry(
() => executor.refreshCredentials(credentials, log),
3,
log
);
if (newCredentials?.accessToken || newCredentials?.apiKey) {
log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed for image generation`);
Object.assign(credentials, newCredentials);
if (onCredentialsRefreshed) await onCredentialsRefreshed(newCredentials);
try {
const retryBody = await adapter.buildBody(model, body);
const retryHeaders = adapter.buildHeaders(credentials, retryBody, model, body);
const retryUrl = adapter.buildUrl(model, credentials);
providerResponse = await fetch(retryUrl, {
method: "POST",
headers: retryHeaders,
body: serializeRequestBody(retryBody),
});
} catch {
log?.warn?.("TOKEN", `${provider.toUpperCase()} | retry after refresh failed`);
}
} else {
log?.warn?.("TOKEN", `${provider.toUpperCase()} | refresh failed`);
}
}
if (!providerResponse.ok) {
const { statusCode, message } = await parseUpstreamError(providerResponse);
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
log?.debug?.("IMAGE", `Provider error: ${errMsg}`);
return createErrorResult(statusCode, errMsg);
}
// Parse provider response — adapter may override (codex SSE / async polling / binary)
let parsed;
try {
if (adapter.parseResponse) {
parsed = await adapter.parseResponse(providerResponse, {
headers,
log,
streamToClient,
onRequestSuccess,
url,
requestBody,
model,
body,
});
// Codex streaming case: returns an SSE Response directly
if (parsed?.sseResponse) {
return { success: true, response: parsed.sseResponse };
}
} else {
parsed = await providerResponse.json();
}
} catch (parseError) {
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, parseError.message || `Invalid response from ${provider}`);
}
if (onRequestSuccess) await onRequestSuccess();
// Normalize → OpenAI-compatible shape
const normalized = adapter.normalize(parsed, body.prompt);
// Already in OpenAI shape? skip re-normalize
const finalBody = (normalized.created && Array.isArray(normalized.data)) ? normalized : parsed;
// Binary output: decode first b64_json (or fetch url) into raw bytes
if (binaryOutput) {
const first = finalBody.data?.[0];
let b64 = first?.b64_json;
if (!b64 && first?.url) {
try { b64 = await urlToBase64(first.url); } catch {}
}
if (b64) {
const buf = Buffer.from(b64, "base64");
const fmt = (body.output_format || "png").toLowerCase();
const mime = fmt === "jpeg" || fmt === "jpg" ? "image/jpeg" : fmt === "webp" ? "image/webp" : "image/png";
return {
success: true,
response: new Response(buf, {
headers: {
"Content-Type": mime,
"Content-Disposition": `inline; filename="image.${fmt === "jpeg" ? "jpg" : fmt}"`,
"Access-Control-Allow-Origin": "*",
},
}),
};
}
}
return {
success: true,
response: new Response(JSON.stringify(finalBody), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}),
};
}
|