Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
lapoaiscalers 6 months ago
parent
commit
e331111d5c
  1. 112
      src/app/api/generate/route.ts

112
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<string, unknown> = {
model: modelId,
};
// Build the input object (all parameters go inside "input" for Kie API)
const inputParams: Record<string, unknown> = {};
// 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<string, unknown>;
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<string, unknown> | 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) {

Loading…
Cancel
Save