From e331111d5cb7ba5fc5e41bd09271108a10ceeeb2 Mon Sep 17 00:00:00 2001 From: lapoaiscalers Date: Thu, 29 Jan 2026 17:20:52 +0100 Subject: [PATCH] fix: Kie.ai API integration - correct upload and request format - Use multipart/form-data for file upload with 'file' and 'uploadPath' fields - Extract download URL from data.downloadUrl in upload response - Wrap parameters in 'input' object for standard models - Use flat structure with camelCase for Veo models - Extract results from resultJson.resultUrls in poll response Co-Authored-By: Claude --- src/app/api/generate/route.ts | 112 ++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 3bdd5e0d..f87035c5 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -1275,6 +1275,7 @@ function isKieVeoModel(modelId: string): boolean { /** * Upload a base64 image to Kie.ai and get a URL * Required for image-to-image models since Kie doesn't accept base64 directly + * Uses multipart/form-data with 'file' and 'uploadPath' fields */ async function uploadImageToKie( requestId: string, @@ -1302,14 +1303,38 @@ async function uploadImageToKie( console.log(`[API:${requestId}] Uploading image to Kie.ai: ${filename} (${(binaryData.length / 1024).toFixed(1)}KB)`); + // Build multipart form data manually + const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`; + const parts: Buffer[] = []; + + // Add file field + parts.push(Buffer.from( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` + + `Content-Type: ${mimeType}\r\n\r\n` + )); + parts.push(binaryData); + parts.push(Buffer.from('\r\n')); + + // Add uploadPath field + parts.push(Buffer.from( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="uploadPath"\r\n\r\n` + + `images\r\n` + )); + + // End boundary + parts.push(Buffer.from(`--${boundary}--\r\n`)); + + const body = Buffer.concat(parts); + const response = await fetch("https://kieai.redpandaai.co/api/file-stream-upload", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, - "Content-Type": mimeType, - "X-Filename": filename, + "Content-Type": `multipart/form-data; boundary=${boundary}`, }, - body: binaryData, + body: body, }); if (!response.ok) { @@ -1318,10 +1343,14 @@ async function uploadImageToKie( } const result = await response.json(); - const downloadUrl = result.downloadUrl || result.url; + console.log(`[API:${requestId}] Kie upload response:`, JSON.stringify(result).substring(0, 300)); + + // Response format: { success: true, code: 200, data: { downloadUrl: "...", fileName: "...", fileSize: 123 } } + const downloadUrl = result.data?.downloadUrl || result.downloadUrl || result.url; if (!downloadUrl) { - throw new Error("No download URL in upload response"); + console.error(`[API:${requestId}] Upload response has no URL:`, result); + throw new Error(`No download URL in upload response. Response: ${JSON.stringify(result).substring(0, 200)}`); } console.log(`[API:${requestId}] Image uploaded: ${downloadUrl.substring(0, 80)}...`); @@ -1398,19 +1427,17 @@ async function generateWithKie( console.log(`[API:${requestId}] Kie.ai generation - Model: ${modelId}, Images: ${input.images?.length || 0}, Prompt: ${input.prompt.length} chars, Veo: ${isVeo}`); - // Build request body - const requestBody: Record = { - model: modelId, - }; + // Build the input object (all parameters go inside "input" for Kie API) + const inputParams: Record = {}; // Add prompt if (input.prompt) { - requestBody.prompt = input.prompt; + inputParams.prompt = input.prompt; } // Add model parameters if (input.parameters) { - Object.assign(requestBody, input.parameters); + Object.assign(inputParams, input.parameters); } // Handle image inputs @@ -1429,16 +1456,8 @@ async function generateWithKie( // Set the correct parameter name for this model const imageKey = getKieImageInputKey(modelId); - if (isVeo) { - // Veo models use camelCase and array format - requestBody[imageKey] = imageUrls; - } else if (modelId === "nano-banana-pro") { - // nano-banana-pro uses single image - requestBody[imageKey] = imageUrls[0]; - } else { - // Most models use array format - requestBody[imageKey] = imageUrls; - } + // All image params are arrays in Kie API + inputParams[imageKey] = imageUrls; } // Handle dynamic inputs (from schema-mapped connections) @@ -1448,21 +1467,49 @@ async function generateWithKie( // Check if this is an image input that needs uploading if (typeof value === 'string' && value.startsWith('data:image')) { const url = await uploadImageToKie(requestId, apiKey, value); - requestBody[key] = url; + // Image params should be arrays + inputParams[key] = [url]; } else { - requestBody[key] = value; + inputParams[key] = value; } } } } + // Build request body - Veo uses different format (no input wrapper, camelCase) + let requestBody: Record; + + if (isVeo) { + // Veo format: { model, prompt, aspectRatio, imageUrls, generationType } + requestBody = { + model: modelId, + prompt: inputParams.prompt, + aspectRatio: inputParams.aspectRatio || inputParams.aspect_ratio || "16:9", + generationType: inputParams.imageUrls || inputParams.image_urls + ? "FIRST_AND_LAST_FRAMES_2_VIDEO" + : "TEXT_2_VIDEO", + }; + // Add image URLs if present + if (inputParams.imageUrls) { + requestBody.imageUrls = inputParams.imageUrls; + } else if (inputParams.image_urls) { + requestBody.imageUrls = inputParams.image_urls; + } + } else { + // Standard format: { model, input: { ... } } + requestBody = { + model: modelId, + input: inputParams, + }; + } + // Select endpoint based on model type const createUrl = isVeo ? "https://api.kie.ai/api/v1/veo/generate" : "https://api.kie.ai/api/v1/jobs/createTask"; console.log(`[API:${requestId}] Calling Kie.ai API: ${createUrl}`); - console.log(`[API:${requestId}] Request body keys: ${Object.keys(requestBody).join(", ")}`); + console.log(`[API:${requestId}] Request body:`, JSON.stringify(requestBody).substring(0, 500)); // Create task const createResponse = await fetch(createUrl, { @@ -1521,14 +1568,25 @@ async function generateWithKie( } // Extract output URL from result + // Kie API returns: { data: { status: "success", resultJson: { resultUrls: ["url1", "url2"] } } } const data = pollResult.data; let mediaUrl: string | null = null; let isVideo = false; - // Try various response formats + console.log(`[API:${requestId}] Kie poll result data:`, JSON.stringify(data).substring(0, 500)); + + // Try various response formats - Kie uses resultJson.resultUrls if (data) { - // Video outputs - if (data.videoUrl) { + const resultJson = data.resultJson as Record | undefined; + const resultUrls = (resultJson?.resultUrls || data.resultUrls) as string[] | undefined; + + if (resultUrls && resultUrls.length > 0) { + mediaUrl = resultUrls[0]; + // Check if it's a video based on URL + isVideo = mediaUrl.includes('.mp4') || mediaUrl.includes('.webm') || mediaUrl.includes('video'); + } + // Fallback to other formats + else if (data.videoUrl) { mediaUrl = data.videoUrl as string; isVideo = true; } else if (data.video_url) {