From 70be28025bbaf5721d71e38b3f8ce8fe2c796661 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 15:17:45 +1200 Subject: [PATCH 1/8] feat: add singular-key handling for Seedance/Wan frame URL params Add first_frame_url, last_frame_url, and first_clip_url to the singular-key checks in generateWithKie() so these params receive a single string URL instead of being wrapped in an array. Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index c33d005f..82326010 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -432,7 +432,7 @@ export async function generateWithKie( // Single data URL - upload it const url = await uploadImageToKie(requestId, apiKey, value); // Singular keys get a string, plural keys get an array - if (key === "image_url" || key === "video_url" || key === "tail_image_url") { + if (key === "image_url" || key === "video_url" || key === "tail_image_url" || key === "first_frame_url" || key === "last_frame_url" || key === "first_clip_url") { inputParams[key] = url; } else { inputParams[key] = [url]; @@ -453,7 +453,7 @@ export async function generateWithKie( } if (processedArray.length > 0) { // Singular keys get first element, plural keys get full array - if (key === "image_url" || key === "video_url" || key === "tail_image_url") { + if (key === "image_url" || key === "video_url" || key === "tail_image_url" || key === "first_frame_url" || key === "last_frame_url" || key === "first_clip_url") { inputParams[key] = processedArray[0]; } else { inputParams[key] = processedArray; @@ -483,7 +483,7 @@ export async function generateWithKie( } // Some models use singular string, others use arrays - if (imageKey === "image_url" || imageKey === "video_url") { + if (imageKey === "image_url" || imageKey === "video_url" || imageKey === "first_frame_url" || imageKey === "last_frame_url" || imageKey === "first_clip_url") { inputParams[imageKey] = imageUrls[0]; } else { inputParams[imageKey] = imageUrls; From d4e99c0d8404b1b2fc9f913cffe8b879d426abcc Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 15:19:25 +1200 Subject: [PATCH 2/8] feat: add 7 new Kie.ai image models Add Nano Banana 2 (Kie), Imagen 4, Imagen 4 Fast, Imagen 4 Ultra, Seedream 5.0 Lite, Seedream 5.0 Lite Edit, and Wan 2.7 Image with registry entries, parameter schemas, defaults, and image input keys. Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 36 +++++++++++++++ src/app/api/models/[modelId]/route.ts | 59 +++++++++++++++++++++++++ src/app/api/models/route.ts | 63 +++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 82326010..0895e6a6 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -39,6 +39,13 @@ export function getKieModelDefaults(modelId: string): Record { quality: "basic", }; + // Nano Banana 2 (Kie) + case "nano-banana-2": + return { + aspect_ratio: "auto", + resolution: "1K", + }; + // Nano Banana Pro (Kie) case "nano-banana-pro": return { @@ -55,6 +62,32 @@ export function getKieModelDefaults(modelId: string): Record { aspect_ratio: "1:1", }; + // Imagen 4 models + case "google/imagen4": + case "google/imagen4-ultra": + return {}; + + case "google/imagen4-fast": + return { + aspect_ratio: "16:9", + num_images: 1, + }; + + // Seedream 5.0 Lite models + case "seedream/5-lite-text-to-image": + case "seedream/5-lite-image-to-image": + return { + aspect_ratio: "1:1", + quality: "basic", + }; + + // Wan 2.7 Image + case "wan/2-7-image": + return { + resolution: "2K", + n: 4, + }; + // Grok Imagine image models case "grok-imagine/text-to-image": return { @@ -158,11 +191,14 @@ export function getKieModelDefaults(modelId: string): Record { */ export function getKieImageInputKey(modelId: string): string { // Model-specific parameter names + if (modelId === "nano-banana-2") return "image_input"; if (modelId === "nano-banana-pro") return "image_input"; if (modelId === "seedream/4.5-edit") return "image_urls"; if (modelId === "gpt-image/1.5-image-to-image") return "input_urls"; // Flux-2 I2I models use input_urls if (modelId === "flux-2/pro-image-to-image" || modelId === "flux-2/flex-image-to-image") return "input_urls"; + // Wan 2.7 Image uses input_urls + if (modelId === "wan/2-7-image") return "input_urls"; // Kling 2.5 turbo I2V uses singular image_url if (modelId === "kling/v2-5-turbo-image-to-video-pro") return "image_url"; // Kling 2.6 motion control uses input_urls diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index ec28e6b8..9c95c4c7 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -748,6 +748,65 @@ function getKieSchema(modelId: string): ExtractedSchema { { name: "image_input", type: "image", required: false, label: "Image", isArray: true }, ], }, + "nano-banana-2": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "2:3", "3:2", "4:3", "16:9", "9:16", "21:9", "auto"], default: "auto" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["512", "1K", "2K", "4K"], default: "1K" }, + ], + inputs: [ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + { name: "image_input", type: "image", required: false, label: "Image", isArray: true }, + ], + }, + "google/imagen4": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "3:4", "4:3", "9:16", "16:9"], default: "1:1" }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "google/imagen4-fast": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "3:4", "4:3", "9:16", "16:9"], default: "16:9" }, + { name: "num_images", type: "integer", description: "Number of images to generate", default: 1, minimum: 1, maximum: 4 }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "google/imagen4-ultra": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "3:4", "4:3", "9:16", "16:9"], default: "1:1" }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "seedream/5-lite-text-to-image": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"], default: "1:1" }, + { name: "quality", type: "string", description: "Output quality", enum: ["basic", "high"], default: "basic" }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "seedream/5-lite-image-to-image": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"], default: "1:1" }, + { name: "quality", type: "string", description: "Output quality", enum: ["basic", "high"], default: "basic" }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + { name: "image_urls", type: "image", required: true, label: "Image", isArray: true }, + ], + }, + "wan/2-7-image": { + parameters: [ + { name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K"], default: "2K" }, + { name: "n", type: "integer", description: "Number of images to generate", default: 4, minimum: 1, maximum: 8 }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + { name: "input_urls", type: "image", required: false, label: "Image", isArray: true }, + ], + }, "grok-imagine/text-to-image": { parameters: [ { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["2:3", "3:2", "1:1", "16:9", "9:16"], default: "1:1" }, diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index de9b3745..b9923e8e 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -155,6 +155,69 @@ const KIE_MODELS: ProviderModel[] = [ coverImage: undefined, pageUrl: "https://docs.kie.ai/market/google/pro-image-to-image", }, + { + id: "nano-banana-2", + name: "Nano Banana 2 (Kie)", + description: "Google Gemini 3.1 Flash image generation via Kie.ai. Supports text-to-image and image-to-image with resolution control.", + provider: "kie", + capabilities: ["text-to-image", "image-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/google/nano-banana-2", + }, + { + id: "google/imagen4", + name: "Imagen 4", + description: "Google Imagen 4 high-quality text-to-image generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/google/imagen4", + }, + { + id: "google/imagen4-fast", + name: "Imagen 4 Fast", + description: "Google Imagen 4 fast text-to-image generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/google/imagen4-fast", + }, + { + id: "google/imagen4-ultra", + name: "Imagen 4 Ultra", + description: "Google Imagen 4 Ultra highest-quality text-to-image generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/google/imagen4-ultra", + }, + { + id: "seedream/5-lite-text-to-image", + name: "Seedream 5.0 Lite", + description: "Seedream 5.0 Lite text-to-image generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/seedream/5-lite", + }, + { + id: "seedream/5-lite-image-to-image", + name: "Seedream 5.0 Lite Edit", + description: "Seedream 5.0 Lite image editing via Kie.ai.", + provider: "kie", + capabilities: ["image-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/seedream/5-lite", + }, + { + id: "wan/2-7-image", + name: "Wan 2.7 Image", + description: "Wan 2.7 image generation. Supports text-to-image and image-to-image via Kie.ai.", + provider: "kie", + capabilities: ["text-to-image", "image-to-image"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/wan/2-7-image", + }, { id: "grok-imagine/text-to-image", name: "Grok Imagine", From 8b4a1b3f4c26ddd88aeb5e54553f081140367c21 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 15:20:47 +1200 Subject: [PATCH 3/8] feat: add Seedance 2.0 and 2.0 Fast video models Add Seedance 2.0 and 2.0 Fast with text-to-video and image-to-video variants. Both support audio generation, web search, and 4-15 second duration. Image input uses first_frame_url (singular string). Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 15 ++++++++ src/app/api/models/[modelId]/route.ts | 50 +++++++++++++++++++++++++++ src/app/api/models/route.ts | 38 +++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 0895e6a6..70a6d5eb 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -97,6 +97,19 @@ export function getKieModelDefaults(modelId: string): Record { case "grok-imagine/image-to-image": return {}; + // Seedance 2.0 models + case "bytedance/seedance-2/text-to-video": + case "bytedance/seedance-2/image-to-video": + case "bytedance/seedance-2-fast/text-to-video": + case "bytedance/seedance-2-fast/image-to-video": + return { + aspect_ratio: "16:9", + resolution: "720p", + duration: 8, + generate_audio: true, + web_search: false, + }; + // Grok Imagine video models case "grok-imagine/text-to-video": return { @@ -199,6 +212,8 @@ export function getKieImageInputKey(modelId: string): string { if (modelId === "flux-2/pro-image-to-image" || modelId === "flux-2/flex-image-to-image") return "input_urls"; // Wan 2.7 Image uses input_urls if (modelId === "wan/2-7-image") return "input_urls"; + // Seedance I2V models use first_frame_url (singular) + if (modelId === "bytedance/seedance-2/image-to-video" || modelId === "bytedance/seedance-2-fast/image-to-video") return "first_frame_url"; // Kling 2.5 turbo I2V uses singular image_url if (modelId === "kling/v2-5-turbo-image-to-video-pro") return "image_url"; // Kling 2.6 motion control uses input_urls diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index 9c95c4c7..960754ba 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -858,6 +858,56 @@ function getKieSchema(modelId: string): ExtractedSchema { inputs: [{ name: "prompt", type: "text", required: true, label: "Sound Description" }], }, // ============ Video models ============ + "bytedance/seedance-2/text-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "720p" }, + { name: "duration", type: "integer", description: "Video duration in seconds (4-15)", default: 8, minimum: 4, maximum: 15 }, + { name: "generate_audio", type: "boolean", description: "Generate audio with the video", default: true }, + { name: "web_search", type: "boolean", description: "Enable web search for prompt enhancement", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "bytedance/seedance-2/image-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "720p" }, + { name: "duration", type: "integer", description: "Video duration in seconds (4-15)", default: 8, minimum: 4, maximum: 15 }, + { name: "generate_audio", type: "boolean", description: "Generate audio with the video", default: true }, + { name: "web_search", type: "boolean", description: "Enable web search for prompt enhancement", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + { name: "first_frame_url", type: "image", required: true, label: "Image" }, + ], + }, + "bytedance/seedance-2-fast/text-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "720p" }, + { name: "duration", type: "integer", description: "Video duration in seconds (4-15)", default: 8, minimum: 4, maximum: 15 }, + { name: "generate_audio", type: "boolean", description: "Generate audio with the video", default: true }, + { name: "web_search", type: "boolean", description: "Enable web search for prompt enhancement", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }, + "bytedance/seedance-2-fast/image-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "720p" }, + { name: "duration", type: "integer", description: "Video duration in seconds (4-15)", default: 8, minimum: 4, maximum: 15 }, + { name: "generate_audio", type: "boolean", description: "Generate audio with the video", default: true }, + { name: "web_search", type: "boolean", description: "Enable web search for prompt enhancement", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + { name: "first_frame_url", type: "image", required: true, label: "Image" }, + ], + }, "grok-imagine/text-to-video": { parameters: [ { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["2:3", "3:2", "1:1", "16:9", "9:16"], default: "2:3" }, diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index b9923e8e..71249e9b 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -236,7 +236,43 @@ const KIE_MODELS: ProviderModel[] = [ coverImage: undefined, pageUrl: "https://kie.ai/grok-imagine", }, - // ============ Video Models (11) ============ + // ============ Video Models ============ + { + id: "bytedance/seedance-2/text-to-video", + name: "Seedance 2.0", + description: "ByteDance Seedance 2.0 text-to-video generation via Kie.ai. Supports audio generation and web search.", + provider: "kie", + capabilities: ["text-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/bytedance/seedance-2", + }, + { + id: "bytedance/seedance-2/image-to-video", + name: "Seedance 2.0 I2V", + description: "ByteDance Seedance 2.0 image-to-video generation via Kie.ai. Supports audio generation and web search.", + provider: "kie", + capabilities: ["image-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/bytedance/seedance-2", + }, + { + id: "bytedance/seedance-2-fast/text-to-video", + name: "Seedance 2.0 Fast", + description: "ByteDance Seedance 2.0 Fast text-to-video generation via Kie.ai. Supports audio generation and web search.", + provider: "kie", + capabilities: ["text-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/bytedance/seedance-2-fast", + }, + { + id: "bytedance/seedance-2-fast/image-to-video", + name: "Seedance 2.0 Fast I2V", + description: "ByteDance Seedance 2.0 Fast image-to-video generation via Kie.ai. Supports audio generation and web search.", + provider: "kie", + capabilities: ["image-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/bytedance/seedance-2-fast", + }, { id: "grok-imagine/text-to-video", name: "Grok Imagine Video", From 285b34eb61a57490419ddc906d0742556fd54df7 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 15:22:33 +1200 Subject: [PATCH 4/8] feat: add Kling 3.0, Kling 3.0 Motion, Wan 2.7 T2V/I2V models Add Kling 3.0 (t2v + i2v), Kling 3.0 Motion Control, Wan 2.7 text-to-video, and Wan 2.7 image-to-video with registry entries, schemas, defaults, and image input keys. Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 34 ++++++++++++++ src/app/api/models/[modelId]/route.ts | 66 +++++++++++++++++++++++++++ src/app/api/models/route.ts | 45 ++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 70a6d5eb..66471808 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -125,6 +125,22 @@ export function getKieModelDefaults(modelId: string): Record { mode: "normal", }; + // Kling 3.0 video models + case "kling-3.0/video/text-to-video": + case "kling-3.0/video/image-to-video": + return { + aspect_ratio: "16:9", + duration: "5", + mode: "pro", + }; + + // Kling 3.0 motion control + case "kling-3.0/motion-control": + return { + mode: "pro", + character_orientation: "video", + }; + // Kling 2.6 video models case "kling-2.6/text-to-video": case "kling-2.6/image-to-video": @@ -164,6 +180,20 @@ export function getKieModelDefaults(modelId: string): Record { resolution: "1080p", }; + // Wan 2.7 video models + case "wan/2-7-text-to-video": + return { + duration: 5, + resolution: "1080p", + ratio: "16:9", + }; + + case "wan/2-7-image-to-video": + return { + duration: 5, + resolution: "1080p", + }; + // Topaz video upscale case "topaz/video-upscale": return { @@ -216,8 +246,12 @@ export function getKieImageInputKey(modelId: string): string { if (modelId === "bytedance/seedance-2/image-to-video" || modelId === "bytedance/seedance-2-fast/image-to-video") return "first_frame_url"; // Kling 2.5 turbo I2V uses singular image_url if (modelId === "kling/v2-5-turbo-image-to-video-pro") return "image_url"; + // Kling 3.0 motion control uses input_urls + if (modelId === "kling-3.0/motion-control") return "input_urls"; // Kling 2.6 motion control uses input_urls if (modelId === "kling-2.6/motion-control") return "input_urls"; + // Wan 2.7 I2V uses first_frame_url (singular) + if (modelId === "wan/2-7-image-to-video") return "first_frame_url"; // Topaz video upscale uses video_url (singular) if (modelId === "topaz/video-upscale") return "video_url"; // Veo 3 models use imageUrls diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index 960754ba..25f1e113 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -961,6 +961,43 @@ function getKieSchema(modelId: string): ExtractedSchema { { name: "video_urls", type: "image", required: true, label: "Video", isArray: true }, ], }, + "kling-3.0/video/text-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "duration", type: "string", description: "Video duration in seconds", enum: ["3", "5", "10", "15"], default: "5" }, + { name: "mode", type: "string", description: "Generation mode", enum: ["std", "pro"], default: "pro" }, + { name: "sound", type: "boolean", description: "Enable sound generation", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + ], + }, + "kling-3.0/video/image-to-video": { + parameters: [ + { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, + { name: "duration", type: "string", description: "Video duration in seconds", enum: ["3", "5", "10", "15"], default: "5" }, + { name: "mode", type: "string", description: "Generation mode", enum: ["std", "pro"], default: "pro" }, + { name: "sound", type: "boolean", description: "Enable sound generation", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + { name: "image_urls", type: "image", required: true, label: "Image", isArray: true }, + ], + }, + "kling-3.0/motion-control": { + parameters: [ + { name: "mode", type: "string", description: "Generation mode", enum: ["std", "pro"], default: "pro" }, + { name: "character_orientation", type: "string", description: "Character orientation source", enum: ["image", "video"], default: "video" }, + { name: "background_source", type: "string", description: "Background source", enum: ["image", "video"], default: "video" }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + { name: "input_urls", type: "image", required: true, label: "Image", isArray: true }, + { name: "video_urls", type: "image", required: true, label: "Video", isArray: true }, + ], + }, "kling/v2-5-turbo-text-to-video-pro": { parameters: [ { name: "aspect_ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" }, @@ -1006,6 +1043,35 @@ function getKieSchema(modelId: string): ExtractedSchema { { name: "image_urls", type: "image", required: true, label: "Image", isArray: true }, ], }, + "wan/2-7-text-to-video": { + parameters: [ + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "1080p" }, + { name: "ratio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1", "4:3", "3:4"], default: "16:9" }, + { name: "duration", type: "integer", description: "Video duration in seconds (2-15)", default: 5, minimum: 2, maximum: 15 }, + { name: "prompt_extend", type: "boolean", description: "Enable prompt extension", default: true }, + { name: "watermark", type: "boolean", description: "Add watermark", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + { name: "negative_prompt", type: "text", required: false, label: "Negative Prompt" }, + ], + }, + "wan/2-7-image-to-video": { + parameters: [ + { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "1080p" }, + { name: "duration", type: "integer", description: "Video duration in seconds (2-15)", default: 5, minimum: 2, maximum: 15 }, + { name: "prompt_extend", type: "boolean", description: "Enable prompt extension", default: true }, + { name: "watermark", type: "boolean", description: "Add watermark", default: false }, + { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, + ], + inputs: [ + { name: "prompt", type: "text", required: false, label: "Prompt" }, + { name: "negative_prompt", type: "text", required: false, label: "Negative Prompt" }, + { name: "first_frame_url", type: "image", required: true, label: "First Frame" }, + { name: "last_frame_url", type: "image", required: false, label: "Last Frame" }, + ], + }, "wan/2-6-video-to-video": { parameters: [ { name: "duration", type: "string", description: "Video duration in seconds", enum: ["5", "10"], default: "5" }, diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index 71249e9b..8580f6fe 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -320,6 +320,33 @@ const KIE_MODELS: ProviderModel[] = [ coverImage: undefined, pageUrl: "https://kie.ai/kling-2-6", }, + { + id: "kling-3.0/video/text-to-video", + name: "Kling 3.0", + description: "Kling 3.0 text-to-video generation via Kie.ai. Supports 3-15 second videos with sound.", + provider: "kie", + capabilities: ["text-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/kling/3-0", + }, + { + id: "kling-3.0/video/image-to-video", + name: "Kling 3.0 I2V", + description: "Kling 3.0 image-to-video generation via Kie.ai. Supports up to 2 reference images.", + provider: "kie", + capabilities: ["image-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/kling/3-0", + }, + { + id: "kling-3.0/motion-control", + name: "Kling 3.0 Motion Control", + description: "Kling 3.0 motion transfer from video to static image via Kie.ai.", + provider: "kie", + capabilities: ["image-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/kling/3-0-motion", + }, { id: "kling/v2-5-turbo-text-to-video-pro", name: "Kling 2.5 Turbo", @@ -367,6 +394,24 @@ const KIE_MODELS: ProviderModel[] = [ coverImage: undefined, pageUrl: "https://kie.ai/wan-2-6", }, + { + id: "wan/2-7-text-to-video", + name: "Wan 2.7", + description: "Wan 2.7 text-to-video generation via Kie.ai. Supports prompt extension and watermark control.", + provider: "kie", + capabilities: ["text-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/wan/2-7-t2v", + }, + { + id: "wan/2-7-image-to-video", + name: "Wan 2.7 I2V", + description: "Wan 2.7 image-to-video generation via Kie.ai. Supports first and last frame control.", + provider: "kie", + capabilities: ["image-to-video"], + coverImage: undefined, + pageUrl: "https://docs.kie.ai/market/wan/2-7-i2v", + }, { id: "topaz/video-upscale", name: "Topaz Video Upscale", From d9f5d4b7e0e7e0f490d6781f609c3b19e205b443 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 20:00:15 +1200 Subject: [PATCH 5/8] fix: support video/audio uploads in Kie media handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dynamic input upload logic only matched data:image URLs, so video data (data:video/mp4) was passed as raw base64 instead of being uploaded to get a hosted URL. This caused "File type not supported" errors for models like Kling 3.0 Motion Control that accept video. - Rename uploadImageToKie → uploadMediaToKie (keep alias for compat) - Rename detectImageType → detectMediaType with video magic bytes and fallback to declared MIME type for non-image content - Broaden data URL checks from data:image to data: in generateWithKie - Set uploadPath to videos/audio/images based on detected MIME type Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 76 ++++++++++++++++++--------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 66471808..79228857 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -262,10 +262,10 @@ export function getKieImageInputKey(modelId: string): string { /** - * Detect actual image type from binary data (magic bytes) + * Detect media type from binary data (magic bytes), with fallback to declared MIME type */ -export function detectImageType(buffer: Buffer): { mimeType: string; ext: string } { - // Check magic bytes +export function detectMediaType(buffer: Buffer, declaredMimeType?: string): { mimeType: string; ext: string } { + // Check image magic bytes if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) { return { mimeType: "image/png", ext: "png" }; } @@ -279,51 +279,72 @@ export function detectImageType(buffer: Buffer): { mimeType: string; ext: string if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) { return { mimeType: "image/gif", ext: "gif" }; } + // Check video magic bytes (MP4: "ftyp" at offset 4) + if (buffer.length > 7 && buffer[4] === 0x66 && buffer[5] === 0x74 && buffer[6] === 0x79 && buffer[7] === 0x70) { + return { mimeType: "video/mp4", ext: "mp4" }; + } + // Check WebM magic bytes (starts with 0x1A 0x45 0xDF 0xA3 - EBML header) + if (buffer[0] === 0x1A && buffer[1] === 0x45 && buffer[2] === 0xDF && buffer[3] === 0xA3) { + return { mimeType: "video/webm", ext: "webm" }; + } + + // Fall back to declared MIME type if magic bytes didn't match + if (declaredMimeType && declaredMimeType !== "image/png") { + const extMap: Record = { + "video/mp4": "mp4", "video/webm": "webm", "video/quicktime": "mov", + "audio/mpeg": "mp3", "audio/wav": "wav", "audio/ogg": "ogg", + "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif", + }; + const ext = extMap[declaredMimeType] || declaredMimeType.split("/")[1] || "bin"; + return { mimeType: declaredMimeType, ext }; + } + // Default to PNG return { mimeType: "image/png", ext: "png" }; } /** - * Upload a base64 image to Kie.ai and get a URL - * Required for image-to-image models since Kie doesn't accept base64 directly + * Upload a base64 media file (image, video, or audio) to Kie.ai and get a URL + * Required because Kie doesn't accept base64 directly — needs hosted URLs * Uses base64 upload endpoint (same as official Kie client) */ -export async function uploadImageToKie( +export async function uploadMediaToKie( requestId: string, apiKey: string, - base64Image: string + base64Media: string ): Promise { // Extract mime type and data from data URL let declaredMimeType = "image/png"; - let imageData = base64Image; + let mediaData = base64Media; - if (base64Image.startsWith("data:")) { - const matches = base64Image.match(/^data:([^;]+);base64,(.+)$/); + if (base64Media.startsWith("data:")) { + const matches = base64Media.match(/^data:([^;]+);base64,(.+)$/); if (matches) { declaredMimeType = matches[1]; - imageData = matches[2]; + mediaData = matches[2]; } } // Convert base64 to binary to detect actual type - const binaryData = Buffer.from(imageData, "base64"); + const binaryData = Buffer.from(mediaData, "base64"); if (binaryData.length > MAX_UPLOAD_SIZE) { - throw new Error(`[API:${requestId}] Image too large to upload (${(binaryData.length / (1024 * 1024)).toFixed(1)}MB, max ${MAX_UPLOAD_SIZE / (1024 * 1024)}MB)`); + throw new Error(`[API:${requestId}] File too large to upload (${(binaryData.length / (1024 * 1024)).toFixed(1)}MB, max ${MAX_UPLOAD_SIZE / (1024 * 1024)}MB)`); } - // Detect actual image type from magic bytes (don't trust the declared MIME type) - const detected = detectImageType(binaryData); + // Detect actual media type from magic bytes, falling back to declared MIME type + const detected = detectMediaType(binaryData, declaredMimeType); const mimeType = detected.mimeType; const ext = detected.ext; const filename = `upload_${Date.now()}.${ext}`; + const uploadPath = mimeType.startsWith("video/") ? "videos" : mimeType.startsWith("audio/") ? "audio" : "images"; - console.log(`[API:${requestId}] Uploading image to Kie.ai: ${filename} (${(binaryData.length / 1024).toFixed(1)}KB) [declared: ${declaredMimeType}, actual: ${mimeType}]`); + console.log(`[API:${requestId}] Uploading media to Kie.ai: ${filename} (${(binaryData.length / 1024).toFixed(1)}KB) [declared: ${declaredMimeType}, actual: ${mimeType}, path: ${uploadPath}]`); // Use base64 upload endpoint (same as official Kie client) // Format: data:{mime_type};base64,{data} - const dataUrl = `data:${mimeType};base64,${imageData}`; + const dataUrl = `data:${mimeType};base64,${mediaData}`; const response = await fetch("https://kieai.redpandaai.co/api/file-base64-upload", { method: "POST", @@ -333,7 +354,7 @@ export async function uploadImageToKie( }, body: JSON.stringify({ base64Data: dataUrl, - uploadPath: "images", + uploadPath, fileName: filename, }), }); @@ -359,10 +380,13 @@ export async function uploadImageToKie( 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)}...`); + console.log(`[API:${requestId}] Media uploaded: ${downloadUrl.substring(0, 80)}...`); return downloadUrl; } +/** @deprecated Use uploadMediaToKie instead */ +export const uploadImageToKie = uploadMediaToKie; + /** * Poll Kie.ai task status until completion */ @@ -512,10 +536,10 @@ export async function generateWithKie( if (input.dynamicInputs) { for (const [key, value] of Object.entries(input.dynamicInputs)) { if (value !== null && value !== undefined && value !== '') { - // Check if this is an image input that needs uploading - if (typeof value === 'string' && value.startsWith('data:image')) { + // Check if this is a media input (image/video/audio) that needs uploading + if (typeof value === 'string' && value.startsWith('data:')) { // Single data URL - upload it - const url = await uploadImageToKie(requestId, apiKey, value); + const url = await uploadMediaToKie(requestId, apiKey, value); // Singular keys get a string, plural keys get an array if (key === "image_url" || key === "video_url" || key === "tail_image_url" || key === "first_frame_url" || key === "last_frame_url" || key === "first_clip_url") { inputParams[key] = url; @@ -527,8 +551,8 @@ export async function generateWithKie( // Array of values - check if they're data URLs that need uploading const processedArray: string[] = []; for (const item of value) { - if (typeof item === 'string' && item.startsWith('data:image')) { - const url = await uploadImageToKie(requestId, apiKey, item); + if (typeof item === 'string' && item.startsWith('data:')) { + const url = await uploadMediaToKie(requestId, apiKey, item); processedArray.push(url); } else if (typeof item === 'string' && item.startsWith('http')) { processedArray.push(item); @@ -561,8 +585,8 @@ export async function generateWithKie( if (image.startsWith("http")) { imageUrls.push(image); } else { - // Upload base64 image - const url = await uploadImageToKie(requestId, apiKey, image); + // Upload base64 media + const url = await uploadMediaToKie(requestId, apiKey, image); imageUrls.push(url); } } From ce085da0617146c3af59b1c3f696f7c58fd4e30f Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 20:11:25 +1200 Subject: [PATCH 6/8] fix: pre-populate schema defaults into node parameters When a model schema loads, enum dropdowns showed "Default" but never wrote the actual default value into node.data.parameters. This caused the Kie API to receive missing parameter values, resulting in errors like "mode is not within the range of allowed options." Now a useEffect watches for schema changes and writes any schema-defined defaults into parameters when the corresponding key is undefined. Co-Authored-By: Claude Sonnet 4.5 --- src/components/nodes/ModelParameters.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/nodes/ModelParameters.tsx b/src/components/nodes/ModelParameters.tsx index 646998ed..40ca7496 100644 --- a/src/components/nodes/ModelParameters.tsx +++ b/src/components/nodes/ModelParameters.tsx @@ -154,6 +154,22 @@ function ModelParametersInner({ fetchSchema(); }, [modelId, provider, replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey, onInputsLoaded]); + // Pre-populate schema defaults into parameters + useEffect(() => { + if (schema.length === 0) return; + const defaults: Record = {}; + let hasNewDefaults = false; + for (const param of schema) { + if (param.default !== undefined && parameters[param.name] === undefined) { + defaults[param.name] = param.default; + hasNewDefaults = true; + } + } + if (hasNewDefaults) { + onParametersChange({ ...parameters, ...defaults }); + } + }, [schema, parameters, onParametersChange]); + // Notify parent to resize node when schema loads useEffect(() => { if (schema.length > 0 && onExpandChange) { From 04c6ee02c5d743439bbcd56253bef82363b5621f Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 20:16:42 +1200 Subject: [PATCH 7/8] fix: correct background_source enum values for Kling 3.0 Motion Control Per Kie docs, background_source accepts "input_video"/"input_image", not "image"/"video". The previous wrong values were being sent to the API after schema defaults pre-population, causing request rejection. Also added background_source to getKieModelDefaults() for consistency. Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 1 + src/app/api/models/[modelId]/route.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 79228857..c4a45dad 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -139,6 +139,7 @@ export function getKieModelDefaults(modelId: string): Record { return { mode: "pro", character_orientation: "video", + background_source: "input_video", }; // Kling 2.6 video models diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index 25f1e113..e146fbe5 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -990,7 +990,7 @@ function getKieSchema(modelId: string): ExtractedSchema { parameters: [ { name: "mode", type: "string", description: "Generation mode", enum: ["std", "pro"], default: "pro" }, { name: "character_orientation", type: "string", description: "Character orientation source", enum: ["image", "video"], default: "video" }, - { name: "background_source", type: "string", description: "Background source", enum: ["image", "video"], default: "video" }, + { name: "background_source", type: "string", description: "Background source", enum: ["input_video", "input_image"], default: "input_video" }, ], inputs: [ { name: "prompt", type: "text", required: false, label: "Prompt" }, From 71b9e71d779c4230762caa1be7338158b066c2ed Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 8 Apr 2026 20:25:55 +1200 Subject: [PATCH 8/8] fix: use correct mode values for Kling 3.0 Motion Control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Kie docs have contradictory info — the parameter description says "std"/"pro" but the example request body uses "720p"/"1080p". The API actually accepts "720p"/"1080p" (same as Kling 2.6 Motion Control), rejecting "std"/"pro" with "mode is not within the range of allowed options." Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/generate/providers/kie.ts | 2 +- src/app/api/models/[modelId]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index c4a45dad..1d1a425f 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -137,7 +137,7 @@ export function getKieModelDefaults(modelId: string): Record { // Kling 3.0 motion control case "kling-3.0/motion-control": return { - mode: "pro", + mode: "720p", character_orientation: "video", background_source: "input_video", }; diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index e146fbe5..ba1aa55f 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -988,7 +988,7 @@ function getKieSchema(modelId: string): ExtractedSchema { }, "kling-3.0/motion-control": { parameters: [ - { name: "mode", type: "string", description: "Generation mode", enum: ["std", "pro"], default: "pro" }, + { name: "mode", type: "string", description: "Output resolution", enum: ["720p", "1080p"], default: "720p" }, { name: "character_orientation", type: "string", description: "Character orientation source", enum: ["image", "video"], default: "video" }, { name: "background_source", type: "string", description: "Background source", enum: ["input_video", "input_image"], default: "input_video" }, ],