| 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); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| let parsed; |
| try { |
| if (adapter.parseResponse) { |
| parsed = await adapter.parseResponse(providerResponse, { |
| headers, |
| log, |
| streamToClient, |
| onRequestSuccess, |
| url, |
| requestBody, |
| model, |
| body, |
| }); |
| |
| 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(); |
|
|
| |
| const normalized = adapter.normalize(parsed, body.prompt); |
|
|
| |
| const finalBody = (normalized.created && Array.isArray(normalized.data)) ? normalized : parsed; |
|
|
| |
| 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": "*", |
| }, |
| }), |
| }; |
| } |
|
|