import type { EnabledImageCapability, GenerationStatus } from "@/lib/types"; import { imageDimensionsForEngine } from "@/lib/image-size-presets"; import { isImageModel } from "@/lib/image-models"; export type ImageCreationEngine = "jimeng" | "evolink" | "bailian"; export const EVOLINK_PLATFORM_RESOLUTION = "1K" as const; export type EvolinkImageSettings = { apiKey?: string; baseUrl: string; model: string; quality?: string; }; export type EvolinkImageConfig = EvolinkImageSettings & { apiKey: string; }; export type EvolinkTaskResponse = { id?: string; task_id?: string; status?: string; results?: unknown; data?: unknown; error?: { code?: string | number; message?: string; } | null; message?: string; }; export function getSelectedImageEngine(): ImageCreationEngine { const value = (process.env.IMAGE_CREATION_ENGINE || process.env.IMAGE_PROVIDER || "jimeng").trim().toLowerCase(); return value === "evolink" || value === "bailian" ? value : "jimeng"; } export function getEffectiveImageEngine(capability: EnabledImageCapability, override?: unknown): ImageCreationEngine { const overrideEngine = normalizeImageEngine(override); if (overrideEngine) return overrideEngine; if (capability === "image.generate") return selectedEngineFrom(process.env.IMAGE_GENERATE_ENGINE); return getSelectedImageEngine(); } export function normalizeImageEngine(value: unknown): ImageCreationEngine | undefined { const normalized = typeof value === "string" ? value.trim().toLowerCase() : ""; if (normalized === "evolink") return "evolink"; if (normalized === "bailian") return "bailian"; if (normalized === "jimeng") return "jimeng"; return undefined; } function selectedEngineFrom(value: string | undefined): ImageCreationEngine { const normalized = (value || "").trim().toLowerCase(); if (normalized === "evolink") return "evolink"; if (normalized === "bailian") return "bailian"; if (normalized === "jimeng") return "jimeng"; return getSelectedImageEngine(); } export function getEvolinkImageSettings(): EvolinkImageSettings { return { apiKey: process.env.EVOLINK_API_KEY?.trim() || undefined, baseUrl: (process.env.EVOLINK_BASE_URL || "https://api.evolink.ai").replace(/\/+$/, ""), model: process.env.EVOLINK_IMAGE_MODEL || "gpt-image-2", quality: cleanOptional(process.env.EVOLINK_IMAGE_QUALITY) }; } export function getEvolinkImageConfig(): EvolinkImageConfig | null { const settings = getEvolinkImageSettings(); if (!settings.apiKey) return null; return { ...settings, apiKey: settings.apiKey }; } export function buildEvolinkImagePayload( capability: EnabledImageCapability, input: Record, settings = getEvolinkImageSettings() ): Record { const prompt = String(input.prompt || "").trim(); const imageUrls = asStringArray(input.imageUrls); const nested = asRecord(input.settings) || {}; const requestedModel = input.model; if (requestedModel !== undefined && !isImageModel(requestedModel) && requestedModel !== settings.model) { throw new Error("Unsupported EvoLink image model."); } const model = requestedModel || settings.model; const image25 = model === "gpt-image-2.5-flare" || model === "gpt-image-2.5-sunburst"; if (image25) validateImage25Input(input, nested, imageUrls); const payload: Record = { model, prompt, n: 1 }; if (!payload.prompt) throw new Error("Prompt is required for image generation."); const rawQuality = input.quality ?? nested.quality; const requestedQuality = cleanOptional(typeof rawQuality === "string" ? rawQuality : undefined)?.toLowerCase(); if (image25 && rawQuality !== undefined && (!requestedQuality || !validQuality(requestedQuality))) { throw new Error("Unsupported EvoLink image quality."); } const quality = requestedQuality || cleanOptional(settings.quality)?.toLowerCase() || (image25 ? "medium" : undefined); if (image25 && !validQuality(quality)) throw new Error("Unsupported EvoLink image quality."); if (quality) payload.quality = quality; payload.resolution = EVOLINK_PLATFORM_RESOLUTION; assignSize(payload, { size: input.size ?? nested.size, width: input.width ?? nested.width, height: input.height ?? nested.height }); if (image25 && payload.size === undefined) payload.size = "1:1"; if (imageUrls.length) payload.image_urls = imageUrls; return payload; } export async function submitEvolinkImageTask( payload: Record, config = getEvolinkImageConfig() ): Promise { if (!config) throw new Error("EvoLink API key is not configured."); return callEvolinkApi("/v1/images/generations", config, { method: "POST", body: JSON.stringify(payload) }); } export async function queryEvolinkTask( taskId: string, config = getEvolinkImageConfig() ): Promise { if (!config) throw new Error("EvoLink API key is not configured."); return callEvolinkApi(`/v1/tasks/${encodeURIComponent(taskId)}`, config, { method: "GET" }); } export function getEvolinkTaskId(response: EvolinkTaskResponse): string | undefined { const data = asRecord(response.data); return firstString(response.id, response.task_id, data?.id, data?.task_id); } export function mapEvolinkStatus(response: EvolinkTaskResponse): GenerationStatus { const status = String(response.status || asRecord(response.data)?.status || "").toLowerCase(); if (["completed", "complete", "succeeded", "success", "done"].includes(status)) return "succeeded"; if (["running", "processing", "generating", "in_progress"].includes(status)) return "running"; if (["queued", "pending", "created", "waiting", "in_queue"].includes(status)) return "queued"; if (["expired", "not_found"].includes(status)) return "expired"; if (["cancelled", "canceled"].includes(status)) return "cancelled"; if (["failed", "error"].includes(status)) return "failed"; return "running"; } export function extractEvolinkResultUrls(response: EvolinkTaskResponse): string[] { const data = asRecord(response.data); const candidates = [ response.results, data?.results, data?.images, data?.image_urls, data?.output, data?.outputs ]; const urls = new Set(); for (const candidate of candidates) collectUrls(candidate, urls); return [...urls]; } function cleanOptional(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized || undefined; } function validQuality(value: unknown): value is "low" | "medium" | "high" { return value === "low" || value === "medium" || value === "high"; } function validateImage25Input(input: Record, nested: Record, imageUrls: string[]) { if (imageUrls.length > 16) throw new Error("EvoLink Image 2.5 supports at most 16 reference images."); for (const resolution of [input.resolution, nested.resolution]) { if (resolution !== undefined && (typeof resolution !== "string" || resolution.trim().toUpperCase() !== "1K")) { throw new Error("EvoLink Image 2.5 resolution must be 1K."); } } for (const key of ["n", "imageCount"]) { for (const value of [input[key], nested[key]]) { if (value !== undefined && !isOne(value)) throw new Error(`EvoLink Image 2.5 ${key} must be 1.`); } } if (input.force_single === false || nested.force_single === false) throw new Error("EvoLink Image 2.5 requires force_single."); for (const size of [input.size, nested.size]) { if (size !== undefined && (typeof size !== "string" || !supportedImage25Sizes.has(size.trim()))) { throw new Error("EvoLink Image 2.5 size must be a supported 1K aspect ratio or A4 size."); } } const width = input.width ?? nested.width; const height = input.height ?? nested.height; if ((width === undefined) !== (height === undefined)) throw new Error("EvoLink Image 2.5 requires both width and height."); if (width !== undefined) { const w = asPositiveInteger(width); const h = asPositiveInteger(height); if (!w || !h || w > 8192 || h > 8192) throw new Error("Invalid EvoLink Image 2.5 dimensions."); const mapped = imageDimensionsForEngine({ width: w, height: h }, "evolink"); const ratio = ratioFromDimensions(mapped.width, mapped.height); const a4 = (mapped.width === 848 && mapped.height === 1200) || (mapped.width === 1200 && mapped.height === 848); const pixels = w * h; const custom = w % 16 === 0 && h % 16 === 0 && pixels >= 655360 && pixels <= 1048576 && Math.max(w, h) / Math.min(w, h) <= 3; if (!ratio && !a4 && !custom) throw new Error("EvoLink Image 2.5 custom dimensions exceed the supported 1K size."); } } function isOne(value: unknown): boolean { return (typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim() !== "") && Number(value) === 1; } function assignSize(payload: Record, input: Record) { if (typeof input.size === "string" && input.size.trim()) { payload.size = input.size.trim(); return; } const width = asPositiveInteger(input.width); const height = asPositiveInteger(input.height); if (!width || !height) return; const dimensions = imageDimensionsForEngine({ width, height }, "evolink"); payload.size = ratioFromDimensions(dimensions.width, dimensions.height) || `${dimensions.width}x${dimensions.height}`; } function asStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.map(String).map((item) => item.trim()).filter(Boolean); } function asPositiveInteger(value: unknown): number | undefined { if (typeof value !== "number" && (typeof value !== "string" || value.trim() === "")) return undefined; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) return undefined; return parsed; } function firstString(...values: unknown[]): string | undefined { for (const value of values) { if (typeof value === "string" && value.trim()) return value.trim(); } return undefined; } function ratioFromDimensions(width: number, height: number): string | undefined { const divisor = gcd(width, height); const ratio = `${width / divisor}:${height / divisor}`; return supportedRatios.has(ratio) ? ratio : undefined; } function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); } const supportedRatios = new Set([ "1:1", "1:2", "2:1", "1:3", "3:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "9:21", "21:9" ]); const supportedImage25Sizes = new Set([...supportedRatios, "848x1200", "1200x848"]); function asRecord(value: unknown): Record | undefined { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : undefined; } function collectUrls(value: unknown, urls: Set) { if (!value) return; if (typeof value === "string") { if (/^https?:\/\//.test(value)) urls.add(value); return; } if (Array.isArray(value)) { for (const item of value) collectUrls(item, urls); return; } const record = asRecord(value); if (!record) return; for (const key of ["url", "image_url", "imageUrl", "result_url", "resultUrl"]) { collectUrls(record[key], urls); } } async function callEvolinkApi( path: string, config: EvolinkImageConfig, init: RequestInit ): Promise { const headers = new Headers(init.headers); headers.set("Content-Type", "application/json"); headers.set("Authorization", `Bearer ${config.apiKey}`); const response = await fetch(`${config.baseUrl}${path}`, { ...init, headers }); const text = await response.text(); let json: unknown; try { json = text ? JSON.parse(text) : {}; } catch { throw new Error(`EvoLink returned non-JSON response: ${response.status} ${text.slice(0, 240)}`); } if (!response.ok) { const message = typeof json === "object" && json && "message" in json ? String(json.message) : text; throw new Error(`EvoLink HTTP ${response.status}: ${message}`); } return json as T; }