import "server-only"; import { appConfig } from "@/lib/config"; import type { GenerateOutputFormat, GeneratePayload, GenerateResult, GeneratedImage, HttpError, } from "@/lib/types"; interface OpenAiImageEntry { b64_json?: string; revised_prompt?: string | null; } interface OpenAiErrorBody { message?: string; [key: string]: unknown; } interface OpenAiImageResponse { created?: number | null; data?: OpenAiImageEntry[]; error?: OpenAiErrorBody; usage?: unknown; } export async function generateImagesWithOpenAI( payload: GeneratePayload, ): Promise { const startedAt = Date.now(); const response = await fetch( `${appConfig.openAiBaseUrl.replace(/\/$/, "")}/images/generations`, { method: "POST", headers: { Authorization: `Bearer ${appConfig.openAiApiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), signal: AbortSignal.timeout(appConfig.openAiTimeoutMs), }, ); const responseBody = await parseJsonSafely(response); if (!response.ok) { const error = new Error( responseBody?.error?.message ?? `OpenAI 返回了 ${response.status} 错误。`, ) as HttpError; error.statusCode = response.status; error.detail = responseBody?.error ?? null; throw error; } const images = Array.isArray(responseBody?.data) ? responseBody.data .map((entry, index) => mapGeneratedImage(entry, index, payload)) .filter((image): image is GeneratedImage => image !== null) : []; if (images.length === 0) { const error = new Error( "上游已返回成功,但没有拿到图片数据。", ) as HttpError; error.statusCode = 502; throw error; } return { images, meta: { model: payload.model, size: payload.size, quality: payload.quality, outputFormat: payload.output_format, background: payload.background, moderation: payload.moderation, outputCompression: payload.output_compression, count: images.length, durationMs: Date.now() - startedAt, created: responseBody?.created ?? null, usage: responseBody?.usage ?? null, }, }; } async function parseJsonSafely( response: Response, ): Promise { try { return (await response.json()) as OpenAiImageResponse; } catch { return null; } } function mapGeneratedImage( entry: OpenAiImageEntry, index: number, payload: GeneratePayload, ): GeneratedImage | null { if (!entry?.b64_json) { return null; } const fileExtension = payload.output_format === "jpeg" ? "jpg" : payload.output_format; const mimeType = resolveMimeType(payload.output_format); return { id: `image-${index + 1}`, base64: entry.b64_json, mimeType, fileName: `gpt-image-${Date.now()}-${index + 1}.${fileExtension}`, revisedPrompt: entry.revised_prompt ?? null, }; } function resolveMimeType(outputFormat: GenerateOutputFormat): string { if (outputFormat === "jpeg") { return "image/jpeg"; } if (outputFormat === "webp") { return "image/webp"; } return "image/png"; }