import "server-only"; import sharp from "sharp"; import { getKreaModelPresetById, inferKreaModelKind, type KreaModelKind, } from "@/lib/ai/krea-models"; import { uploadFile } from "@/lib/storage/service"; interface KreaConfig { apiKey: string; baseURL: string; } interface KreaImageParams { aspectRatio?: string; imageSize?: string; imageCount?: number; seed?: number; negativePrompt?: string; promptExtend?: boolean; } interface KreaVideoParams { mode?: "text-to-video" | "first-frame" | "first-last-frame" | "reference-images"; ratio?: string; duration?: number; resolution?: string; seed?: number; generateAudio?: boolean; } interface UploadedImage { thumbnail: string; original: string; } const DEFAULT_BASE_URL = "https://api.krea.ai"; const IMAGE_POLL_INTERVAL_MS = 2000; const VIDEO_POLL_INTERVAL_MS = 5000; const IMAGE_TIMEOUT_MS = 5 * 60 * 1000; const VIDEO_TIMEOUT_MS = 20 * 60 * 1000; function normalizeText(value?: string | null) { return String(value || "").replace(/\u00a0/g, " ").trim().normalize("NFKC"); } function normalizeBaseURL(baseURL: string) { const normalized = normalizeText(baseURL).replace(/\/+$/, ""); return normalized || DEFAULT_BASE_URL; } function normalizeKreaApiKey(apiKey: string) { const stripped = normalizeText(apiKey) .replace(/^bearer\s+/i, "") .replace(/^key\s+/i, "") .replace(/^(?:krea[_-]?api[_-]?key|api[_-]?key|token)\s*[::]\s*/i, ""); return stripped.trim().replace(/[^\x20-\x7E]/g, ""); } function buildHeaders(apiKey: string) { return { Authorization: `Bearer ${normalizeKreaApiKey(apiKey)}`, "Content-Type": "application/json", Accept: "application/json", }; } function joinEndpoint(baseURL: string, path: string) { const cleanPath = path.startsWith("/") ? path : `/${path}`; return `${normalizeBaseURL(baseURL)}${cleanPath}`; } function resolveKreaEndpoint(modelId: string) { const normalized = normalizeText(modelId); if (!normalized) { throw new Error("Krea 模型 ID 不能为空"); } if (normalized.startsWith("/generate/")) return normalized; if (normalized.startsWith("generate/")) return `/${normalized}`; if (/^(image|video|enhance)\//i.test(normalized)) return `/generate/${normalized}`; throw new Error("Krea 模型 ID 需要使用官方 /generate/... endpoint"); } function detectExtensionFromContentType(contentType?: string | null) { const normalized = String(contentType || "").toLowerCase(); if (normalized.includes("jpeg") || normalized.includes("jpg")) return "jpg"; if (normalized.includes("webp")) return "webp"; if (normalized.includes("gif")) return "gif"; if (normalized.includes("png")) return "png"; if (normalized.includes("mp4")) return "mp4"; if (normalized.includes("quicktime")) return "mov"; if (normalized.includes("webm")) return "webm"; return "bin"; } function generateFilename(prefix: string, extension: string) { const timestamp = Date.now(); const random = Math.random().toString(36).slice(2, 8); return `${prefix}-${timestamp}-${random}.${extension}`; } function extractDataUriParts(dataUri: string) { const match = dataUri.match(/^data:([^;]+);base64,(.+)$/); if (!match) { throw new Error("不支持的 Data URI 格式"); } return { mimeType: match[1], buffer: Buffer.from(match[2], "base64"), }; } async function uploadDataUriAsset(dataUri: string, prefix: "krea-input" | "krea-video") { const { mimeType, buffer } = extractDataUriParts(dataUri); const extension = detectExtensionFromContentType(mimeType); const arrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; const uploaded = await uploadFile(generateFilename(prefix, extension), arrayBuffer); return uploaded.url; } async function normalizeAssetUrl(url?: string | null) { const normalized = normalizeText(url); if (!normalized) return ""; if (/^https?:\/\//i.test(normalized)) return normalized; if (/^data:(image|video)\//i.test(normalized)) { return uploadDataUriAsset( normalized, /^data:video\//i.test(normalized) ? "krea-video" : "krea-input" ); } return normalized; } function isVideoUrl(url: string, mediaType?: string | null) { const normalizedMediaType = String(mediaType || "").toLowerCase(); if (normalizedMediaType.startsWith("video/")) return true; return /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(url) || /^data:video\//i.test(url); } function extractPromptAndAssets(messages: Array<{ role: string; content: any }>) { const lastUserMessage = messages.filter((message) => message.role === "user").pop(); if (!lastUserMessage) { throw new Error("未找到用户输入内容"); } let prompt = ""; const imageUrls: string[] = []; const videoUrls: string[] = []; const pushAssetUrl = (value?: string, mediaType?: string | null) => { if (!value) return; const normalized = String(value || "").trim(); if (!normalized) return; const target = isVideoUrl(normalized, mediaType) ? videoUrls : imageUrls; if (!target.includes(normalized)) { target.push(normalized); } }; const readContentPart = (part: any) => { if (!part) return; if (part.type === "text" && typeof part.text === "string") { prompt = `${prompt} ${part.text}`.trim(); return; } if (part.type === "image_url" && part.image_url?.url) { pushAssetUrl(part.image_url.url, "image/jpeg"); return; } if (part.type === "file") { pushAssetUrl(part.url || part.data, part.mediaType || part.mimeType); return; } if (part.type === "image") { pushAssetUrl(part.image || part.url || part.data, part.mediaType || "image/jpeg"); return; } if (part.type === "video") { pushAssetUrl(part.video || part.url || part.data, part.mediaType || "video/mp4"); } }; if (typeof lastUserMessage.content === "string") { prompt = normalizeText(lastUserMessage.content); } else if (Array.isArray(lastUserMessage.content)) { for (const part of lastUserMessage.content) { readContentPart(part); } } return { prompt: normalizeText(prompt) || "生成内容", imageUrls, videoUrls, }; } function normalizeAspectRatio(value?: string, fallback = "16:9") { const normalized = normalizeText(value || fallback); if ( [ "21:9", "16:9", "9:16", "4:3", "3:4", "1:1", "9:21", "auto", ].includes(normalized) ) { return normalized; } return fallback; } function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } function roundToMultiple(value: number, multiple = 16) { return Math.max(multiple, Math.round(value / multiple) * multiple); } function normalizeImageSizeLabel(imageSize?: string): "1K" | "2K" | "4K" { const normalized = normalizeText(imageSize).toUpperCase(); if (normalized === "1K" || normalized === "2K" || normalized === "4K") { return normalized; } return "2K"; } function resolveImageDimensions(aspectRatio?: string, imageSize?: string) { const size = normalizeImageSizeLabel(imageSize); const base = size === "4K" ? 2048 : size === "2K" ? 1536 : 1024; const ratio = normalizeAspectRatio(aspectRatio, "1:1"); if (ratio === "auto" || !ratio.includes(":")) { return { width: base, height: base, imageSize: size }; } const [rawW, rawH] = ratio.split(":").map((item) => Number(item)); const ratioW = Number.isFinite(rawW) && rawW > 0 ? rawW : 1; const ratioH = Number.isFinite(rawH) && rawH > 0 ? rawH : 1; const landscape = ratioW >= ratioH; const width = landscape ? base : Math.round((base * ratioW) / ratioH); const height = landscape ? Math.round((base * ratioH) / ratioW) : base; return { width: clamp(roundToMultiple(width), 256, 2048), height: clamp(roundToMultiple(height), 256, 2048), imageSize: size, }; } function resolveVideoDimensions(ratio?: string, resolution?: string) { const normalizedResolution = normalizeText(resolution).toLowerCase(); const baseHeight = normalizedResolution === "1080p" ? 1080 : 720; const aspectRatio = normalizeAspectRatio(ratio, "16:9"); if (aspectRatio === "9:16") { return { width: baseHeight, height: Math.round((baseHeight * 16) / 9) }; } if (aspectRatio === "1:1") { return { width: baseHeight, height: baseHeight }; } if (aspectRatio === "4:3") { return { width: Math.round((baseHeight * 4) / 3), height: baseHeight }; } if (aspectRatio === "3:4") { return { width: baseHeight, height: Math.round((baseHeight * 4) / 3) }; } if (aspectRatio === "21:9") { return { width: Math.round((baseHeight * 21) / 9), height: baseHeight }; } return { width: Math.round((baseHeight * 16) / 9), height: baseHeight }; } function normalizeSeed(seed?: number) { if (!Number.isFinite(Number(seed))) return undefined; return clamp(Math.round(Number(seed)), 0, 4294967295); } function normalizeBatchSize(value?: number) { if (!Number.isFinite(Number(value))) return 1; return clamp(Math.round(Number(value)), 1, 4); } function normalizeVideoDuration(duration: unknown, endpoint: string) { const value = Number.isFinite(Number(duration)) ? Math.round(Number(duration)) : 5; if (endpoint.includes("/minimax/hailuo-02") || endpoint.includes("/minimax/hailuo-2.3")) { return clamp(value || 6, 6, 10); } if (endpoint.includes("/google/veo")) { return clamp(value || 8, 4, 8); } if (endpoint.includes("/openai/sora-2")) { return clamp(value || 4, 4, 20); } if (endpoint.includes("/kling/")) { return value >= 8 ? 10 : 5; } if (endpoint.includes("/alibaba/wan-2.5")) { return clamp(value || 5, 5, 10); } return value >= 8 ? 10 : 5; } function normalizeVideoResolution(resolution: unknown, endpoint: string) { const normalized = normalizeText(String(resolution || "")).toLowerCase(); if (endpoint.includes("/minimax/")) { if (normalized === "1080p") return "1080p"; if (normalized === "512p" || normalized === "480p") return "512p"; return "768p"; } if (normalized === "1080p" || normalized === "480p") return normalized; return "720p"; } function normalizeVideoAspectRatio(ratio: unknown, endpoint: string) { const normalized = normalizeAspectRatio(String(ratio || ""), "16:9"); if (endpoint.includes("/openai/sora-2")) { return normalized === "9:16" ? "9:16" : "16:9"; } if (endpoint.includes("/google/veo") || endpoint.includes("/kling/")) { return ["16:9", "9:16", "1:1"].includes(normalized) ? normalized : "16:9"; } return normalized === "auto" ? "16:9" : normalized; } function buildStyleImages(imageUrls: string[]) { return imageUrls.map((url) => ({ url, strength: 1 })); } function buildTaggedReferences(imageUrls: string[], tagPrefix = "ref", max = 3) { return imageUrls.slice(0, max).map((url, index) => ({ url, tag: `${tagPrefix}_${index + 1}`.replace(/[^a-z0-9_]/gi, "_").toLowerCase(), })); } function collectHttpUrls(payload: unknown): string[] { const urls = new Set(); const visit = (value: unknown) => { if (typeof value === "string") { const normalized = value.trim(); if (/^https?:\/\//i.test(normalized)) { urls.add(normalized); } return; } if (Array.isArray(value)) { value.forEach(visit); return; } if (value && typeof value === "object") { Object.values(value as Record).forEach(visit); } }; visit(payload); return Array.from(urls); } async function downloadRemoteAsset(url: string) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 120000); try { const response = await fetch(url, { cache: "no-store", signal: controller.signal, }); if (!response.ok) { throw new Error(`下载 Krea 输出失败: ${response.status}`); } return { buffer: Buffer.from(await response.arrayBuffer()), contentType: response.headers.get("content-type"), }; } finally { clearTimeout(timeoutId); } } async function uploadGeneratedBuffer(buffer: Buffer, extension: string): Promise { const thumbnailBuffer = await sharp(buffer) .resize(400, 400, { fit: "inside", withoutEnlargement: true }) .jpeg({ quality: 80 }) .toBuffer(); const originalArrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; const originalUploaded = await uploadFile( generateFilename("krea-image-orig", extension === "bin" ? "png" : extension), originalArrayBuffer ); const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( thumbnailBuffer.byteOffset, thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength ) as ArrayBuffer; const thumbnailUploaded = await uploadFile( generateFilename("krea-image-thumb", "jpg"), thumbnailArrayBuffer ); return { thumbnail: thumbnailUploaded.url, original: originalUploaded.url, }; } async function uploadKreaImages(payload: unknown) { const uploadedImages: UploadedImage[] = []; for (const url of collectHttpUrls(payload)) { try { const { buffer, contentType } = await downloadRemoteAsset(url); if (!String(contentType || "").startsWith("image/")) continue; uploadedImages.push(await uploadGeneratedBuffer(buffer, detectExtensionFromContentType(contentType))); } catch (error) { console.warn("[Krea] Failed to archive image output:", error); } } if (uploadedImages.length === 0) { throw new Error("Krea 未返回可用图片"); } return uploadedImages; } async function uploadVideoToStorage(remoteVideoUrl: string) { try { const { buffer, contentType } = await downloadRemoteAsset(remoteVideoUrl); const extension = detectExtensionFromContentType(contentType || "video/mp4"); const arrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; const uploaded = await uploadFile( generateFilename("krea-video", extension === "bin" ? "mp4" : extension), arrayBuffer ); return uploaded.url; } catch (error) { console.warn("[Krea] Failed to archive video output, using temporary URL:", error); return remoteVideoUrl; } } async function resolveKreaVideoUrl(payload: unknown) { const outputUrls = collectHttpUrls(payload); if (outputUrls.length === 0) { throw new Error("Krea 未返回可用视频地址"); } const videoUrl = outputUrls.find((url) => /\.(mp4|mov|webm)(\?|#|$)/i.test(url) || /video/i.test(url)) || outputUrls[0]!; return uploadVideoToStorage(videoUrl); } function formatImageMarkdown(images: UploadedImage[], imageSize: string) { return images .map((image) => `![thumb|||${image.original}|||${imageSize}](${image.thumbnail})`) .join("\n\n"); } function formatVideoMarkdown(videoUrl: string) { return `\n\n[下载视频](${videoUrl})`; } async function requestKreaJob( baseURL: string, apiKey: string, endpoint: string, body: Record ) { const response = await fetch(joinEndpoint(baseURL, endpoint), { method: "POST", headers: buildHeaders(apiKey), body: JSON.stringify(body), }); const rawText = await response.text(); let payload: any = null; try { payload = rawText ? JSON.parse(rawText) : null; } catch { payload = rawText; } if (!response.ok) { console.error("[Krea] Job request failed:", { endpoint, status: response.status, requestFields: Object.keys(body), response: payload, }); const message = typeof payload?.error === "string" ? payload.error : typeof payload?.message === "string" ? payload.message : rawText || "Krea 请求失败"; throw new Error(`${message} (HTTP ${response.status})`); } const jobId = normalizeText(payload?.job_id || payload?.id); if (!jobId && Array.isArray(payload?.result?.urls) && payload.result.urls.length > 0) { return payload; } if (!jobId) { throw new Error("Krea 未返回任务 ID"); } return { ...payload, job_id: jobId }; } async function waitForKreaJob( baseURL: string, apiKey: string, jobId: string, kind: KreaModelKind ) { const isVideo = kind === "video"; const deadline = Date.now() + (isVideo ? VIDEO_TIMEOUT_MS : IMAGE_TIMEOUT_MS); const pollInterval = isVideo ? VIDEO_POLL_INTERVAL_MS : IMAGE_POLL_INTERVAL_MS; while (Date.now() < deadline) { const response = await fetch(joinEndpoint(baseURL, `/jobs/${jobId}`), { method: "GET", headers: buildHeaders(apiKey), cache: "no-store", }); const rawText = await response.text(); let payload: any = null; try { payload = rawText ? JSON.parse(rawText) : null; } catch { payload = rawText; } if (!response.ok) { const message = typeof payload?.error === "string" ? payload.error : typeof payload?.message === "string" ? payload.message : rawText || "Krea 轮询失败"; throw new Error(`${message} (HTTP ${response.status})`); } const status = normalizeText(payload?.status).toLowerCase(); if (status === "completed") { return payload; } if (status === "failed" || status === "cancelled") { throw new Error(String(payload?.error || payload?.message || "Krea 任务失败")); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } throw new Error("Krea 任务超时,请稍后重试"); } function getRequiredTopazModel(endpoint: string) { if (endpoint.includes("/standard-enhance")) return "Standard V2"; if (endpoint.includes("/bloom-enhance")) return "Reimagine"; if (endpoint.includes("/generative-enhance")) return "Redefine"; return undefined; } function getRequiredModelValue(endpoint: string) { if (endpoint.includes("/seedream-3")) return "seedream-3-0-t2i-250415"; if (endpoint.includes("/minimax/hailuo")) return "video-01"; return getRequiredTopazModel(endpoint); } function buildKreaBody(params: { endpoint: string; kind: KreaModelKind; requestFields: string[]; requiredFields: string[]; prompt: string; imageUrls: string[]; videoUrls: string[]; imageParams?: KreaImageParams; videoParams?: KreaVideoParams; }) { const { endpoint, kind, requestFields, requiredFields, prompt, imageUrls, videoUrls, imageParams, videoParams, } = params; const fields = new Set(requestFields); const required = new Set(requiredFields); const body: Record = {}; const isVideo = kind === "video"; const ratio = isVideo ? normalizeVideoAspectRatio(videoParams?.ratio, endpoint) : normalizeAspectRatio(imageParams?.aspectRatio, "1:1"); const videoResolution = normalizeVideoResolution(videoParams?.resolution, endpoint); const imageDimensions = resolveImageDimensions(imageParams?.aspectRatio, imageParams?.imageSize); const videoDimensions = resolveVideoDimensions(ratio, videoResolution); if (fields.has("prompt")) body.prompt = prompt; if (fields.has("batchSize")) body.batchSize = normalizeBatchSize(imageParams?.imageCount); if (fields.has("width")) body.width = isVideo ? videoDimensions.width : imageDimensions.width; if (fields.has("height")) body.height = isVideo ? videoDimensions.height : imageDimensions.height; if (fields.has("aspectRatio")) body.aspectRatio = ratio === "auto" ? "1:1" : ratio; if (fields.has("ratio")) body.ratio = ratio === "auto" ? "16:9" : ratio; if (fields.has("resolution")) { body.resolution = isVideo ? videoResolution : normalizeImageSizeLabel(imageParams?.imageSize); } const seed = normalizeSeed(isVideo ? videoParams?.seed : imageParams?.seed); if (fields.has("seed") && seed !== undefined) body.seed = seed; if (fields.has("negative_prompt") && imageParams?.negativePrompt) { body.negative_prompt = imageParams.negativePrompt; } if (fields.has("skipPromptExpansion") && typeof imageParams?.promptExtend === "boolean") { body.skipPromptExpansion = !imageParams.promptExtend; } if (fields.has("enablePromptExpansion")) { body.enablePromptExpansion = true; } if (fields.has("expandPrompt")) { body.expandPrompt = true; } if (fields.has("duration")) { body.duration = normalizeVideoDuration(videoParams?.duration, endpoint); } if (fields.has("generateAudio") && typeof videoParams?.generateAudio === "boolean") { body.generateAudio = videoParams.generateAudio; } if (fields.has("loop")) { body.loop = false; } if (fields.has("mode")) { body.mode = endpoint.includes("/kling/kling-2") ? "master" : "std"; } if (fields.has("model")) { const modelValue = getRequiredModelValue(endpoint); if (modelValue) body.model = modelValue; } if (fields.has("output_format")) body.output_format = "jpg"; if (fields.has("backgroundImage")) body.backgroundImage = "auto"; if (fields.has("imageUrls") && imageUrls.length > 0) { body.imageUrls = imageUrls.slice(0, 8); } if (fields.has("imageUrl") && imageUrls[0]) { body.imageUrl = imageUrls[0]; } if (fields.has("image_url") && imageUrls[0]) { body.image_url = imageUrls[0]; } if (fields.has("styleImages") && imageUrls.length > 0 && !body.imageUrls && !body.imageUrl) { body.styleImages = buildStyleImages(imageUrls.slice(0, 4)); } if (fields.has("imageStyleRefs") && imageUrls.length > 1) { body.imageStyleRefs = buildStyleImages(imageUrls.slice(1, 5)); } if (fields.has("characterReferenceImages") && imageUrls.length > 0) { body.characterReferenceImages = imageUrls.slice(0, 4); } if (fields.has("referenceImages") && imageUrls.length > 0) { body.referenceImages = endpoint.includes("/image/runway/gen-4-image") ? buildTaggedReferences(imageUrls, "ref", 3) : imageUrls.slice(0, endpoint.includes("/seedance-2") ? 9 : 4); } if (fields.has("startImage") && imageUrls[0]) body.startImage = imageUrls[0]; if (fields.has("endImage") && imageUrls[1]) body.endImage = imageUrls[1]; if (fields.has("init_image_url") && imageUrls[0]) body.init_image_url = imageUrls[0]; if (fields.has("references") && imageUrls[0]) { body.references = [{ type: "image", uri: imageUrls[0] }]; } if (fields.has("subjects") && imageUrls[0] && videoParams?.mode === "reference-images") { body.subjects = [{ type: "character", image: [imageUrls[0]] }]; } if (fields.has("styleReferences") && imageUrls.length > 0) { body.styleReferences = buildTaggedReferences(imageUrls, "style", 3); } if (fields.has("elementReferences") && imageUrls.length > 0) { body.elementReferences = [ { tag: "element_1", images: imageUrls.slice(0, 3), }, ]; } if (fields.has("videoReference") && videoUrls[0]) { body.videoReference = { url: videoUrls[0], tag: "video_1" }; } if (fields.has("startVideo") && videoUrls[0]) body.startVideo = videoUrls[0]; if (fields.has("init_video") && videoUrls[0]) body.init_video = videoUrls[0]; if (fields.has("referenceVideos") && videoUrls.length > 0) { body.referenceVideos = videoUrls.slice(0, 3); } if (required.has("referenceImages") && !body.referenceImages) { throw new Error("该 Krea 模型需要至少 1 张参考图"); } if ((required.has("imageUrl") || required.has("image_url")) && !body.imageUrl && !body.image_url) { throw new Error("该 Krea 模型需要上传图片"); } if (required.has("init_video") && !body.init_video) { throw new Error("该 Krea 模型需要上传视频"); } return { body, imageSize: imageDimensions.imageSize }; } export function createKrea({ apiKey, baseURL }: KreaConfig) { return { provider: "krea", chat: (modelId: string) => ({ provider: "krea", doGenerate: async (options: { messages: Array<{ role: string; content: any }>; imageParams?: KreaImageParams; videoParams?: KreaVideoParams; overrideModelId?: string; }) => { try { const activeModelId = normalizeText(options.overrideModelId || modelId); const endpoint = resolveKreaEndpoint(activeModelId); const preset = getKreaModelPresetById(endpoint); const kind = preset?.kind || inferKreaModelKind(endpoint) || (options.videoParams ? "video" : "image"); const { prompt, imageUrls, videoUrls } = extractPromptAndAssets(options.messages); const normalizedImageUrls = ( await Promise.all(imageUrls.map((url) => normalizeAssetUrl(url))) ).filter(Boolean); const normalizedVideoUrls = ( await Promise.all(videoUrls.map((url) => normalizeAssetUrl(url))) ).filter(Boolean); const requestFields = preset?.requestFields || (kind === "video" ? ["prompt", "aspectRatio", "duration", "startImage", "endImage", "referenceImages"] : ["prompt", "width", "height", "imageUrls", "imageUrl", "batchSize"]); const requiredFields = preset?.requiredFields || ["prompt"]; const { body, imageSize } = buildKreaBody({ endpoint, kind, requestFields, requiredFields, prompt, imageUrls: normalizedImageUrls, videoUrls: normalizedVideoUrls, imageParams: options.imageParams, videoParams: options.videoParams, }); console.log("[Krea] Submitting job:", { endpoint, kind, requestFields: Object.keys(body), imageCount: normalizedImageUrls.length, videoCount: normalizedVideoUrls.length, }); const job = await requestKreaJob(baseURL, apiKey, endpoint, body); const output = job?.result?.urls?.length ? job : await waitForKreaJob(baseURL, apiKey, job.job_id, kind); const text = kind === "video" ? formatVideoMarkdown(await resolveKreaVideoUrl(output?.result ?? output)) : formatImageMarkdown(await uploadKreaImages(output?.result ?? output), imageSize); return { text, finishReason: "stop" as const, usage: { promptTokens: 0, completionTokens: text.length, }, }; } catch (error) { console.error("[Krea] Generation failed:", error); const kind = inferKreaModelKind(options.overrideModelId || modelId) || (options.videoParams ? "video" : "image"); return { text: error instanceof Error ? `${kind === "video" ? "视频" : "图片"}生成失败: ${error.message}` : "Krea 生成失败,请稍后重试", finishReason: "error" as const, usage: { promptTokens: 0, completionTokens: 0, }, }; } }, doStream: async function* (options: { messages: Array<{ role: string; content: any }>; imageParams?: KreaImageParams; videoParams?: KreaVideoParams; overrideModelId?: string; }) { const result = await this.doGenerate(options); yield { type: "text-delta", textDelta: result.text, }; }, modelId, specificationVersion: "v1" as const, defaultObjectGenerationMode: "json" as const, }), }; }