Files
image-prompt-studio/lib/openai.ts
2026-04-25 11:26:33 +08:00

131 lines
3.1 KiB
TypeScript

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<GenerateResult> {
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<OpenAiImageResponse | null> {
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";
}