Browse Source

feat: add all missing Kie.ai models + fix model page links

Add ~38 new image/video models (Imagen 4, FLUX.2, Flux Kontext,
Grok Imagine, Ideogram, Qwen, Runway, Hailuo, Seedance variants,
Kling older, Wan older) with schemas and endpoint routing for
dedicated APIs (Flux Kontext, Runway). Add pageUrl to all Kie
models and update ModelSearchDialog to use it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
9806f0ae0a
  1. 169
      src/app/api/generate/route.ts
  2. 277
      src/app/api/models/[modelId]/route.ts
  3. 365
      src/app/api/models/route.ts
  4. 98
      src/components/modals/ModelSearchDialog.tsx
  5. 2
      src/lib/providers/types.ts

169
src/app/api/generate/route.ts

@ -1328,6 +1328,110 @@ function getKieModelDefaults(modelId: string): Record<string, unknown> {
// Wan video models
case "wan/2-6-text-to-video":
case "wan/2-6-image-to-video":
case "wan/2-2-a14b-text-to-video-turbo":
case "wan/2-2-a14b-image-to-video-turbo":
case "wan/2-6-video-to-video":
return {
aspect_ratio: "16:9",
duration: "5",
};
// Imagen 4 models
case "google/imagen4":
case "google/imagen4-fast":
case "google/imagen4-ultra":
return {
aspect_ratio: "1:1",
};
// Google Pro image-to-image
case "google/pro-image-to-image":
return {
aspect_ratio: "1:1",
};
// Flux-2 models
case "flux-2/pro-text-to-image":
case "flux-2/pro-image-to-image":
case "flux-2/flex-text-to-image":
case "flux-2/flex-image-to-image":
return {
aspect_ratio: "1:1",
};
// Flux Kontext models (camelCase params, top-level)
case "flux-kontext-pro":
case "flux-kontext-max":
return {
aspectRatio: "1:1",
outputFormat: "png",
};
// Grok Imagine models
case "grok-imagine/text-to-image":
case "grok-imagine/image-to-image":
return {
aspect_ratio: "1:1",
};
// Grok Imagine video models
case "grok-imagine/text-to-video":
case "grok-imagine/image-to-video":
return {
aspect_ratio: "16:9",
duration: "5",
};
// Ideogram models
case "ideogram/v3-reframe":
case "ideogram/character":
case "ideogram/character-edit":
return {
aspect_ratio: "1:1",
};
// Qwen models
case "qwen/text-to-image":
case "qwen/image-to-image":
case "qwen/image-edit":
return {
aspect_ratio: "1:1",
};
// Runway models (camelCase params, top-level)
case "runway/gen4":
case "runway/aleph":
return {
aspectRatio: "16:9",
duration: 10,
};
// Hailuo video models
case "hailuo/2-3-image-to-video-pro":
case "hailuo/2-3-image-to-video-standard":
case "hailuo/02-text-to-video-pro":
case "hailuo/02-image-to-video-pro":
return {
aspect_ratio: "16:9",
duration: "5",
};
// Bytedance Seedance variants
case "bytedance/v1-pro-text-to-video":
case "bytedance/v1-pro-image-to-video":
case "bytedance/v1-pro-fast-image-to-video":
case "bytedance/v1-lite-text-to-video":
case "bytedance/v1-lite-image-to-video":
return {
aspect_ratio: "16:9",
duration: "5",
};
// Kling older models
case "kling/v2-1-master-text-to-video":
case "kling/v2-1-master-image-to-video":
case "kling/v2-5-turbo-text-to-video-pro":
case "kling/v2-5-turbo-image-to-video-pro":
return {
aspect_ratio: "16:9",
duration: "5",
@ -1347,6 +1451,10 @@ function getKieImageInputKey(modelId: string): string {
if (modelId === "seedream/4.5-edit") return "image_urls";
if (modelId === "gpt-image/1.5-image-to-image") return "input_urls";
if (modelId === "veo3" || modelId === "veo3_fast") return "imageUrls";
// Flux Kontext uses singular image_url
if (modelId.startsWith("flux-kontext-")) return "image_url";
// Runway uses camelCase imageUrl
if (modelId.startsWith("runway/")) return "imageUrl";
// Default for most models
return "image_urls";
}
@ -1358,6 +1466,20 @@ function isKieVeoModel(modelId: string): boolean {
return modelId === "veo3" || modelId === "veo3_fast";
}
/**
* Check if a model is a Flux Kontext model (uses dedicated endpoint)
*/
function isKieFluxKontextModel(modelId: string): boolean {
return modelId.startsWith("flux-kontext-");
}
/**
* Check if a model is a Runway model (uses dedicated endpoint)
*/
function isKieRunwayModel(modelId: string): boolean {
return modelId.startsWith("runway/");
}
/**
* Detect actual image type from binary data (magic bytes)
*/
@ -1524,8 +1646,10 @@ async function generateWithKie(
): Promise<GenerationOutput> {
const modelId = input.model.id;
const isVeo = isKieVeoModel(modelId);
const isFluxKontext = isKieFluxKontextModel(modelId);
const isRunway = isKieRunwayModel(modelId);
console.log(`[API:${requestId}] Kie.ai generation - Model: ${modelId}, Images: ${input.images?.length || 0}, Prompt: ${input.prompt.length} chars, Veo: ${isVeo}`);
console.log(`[API:${requestId}] Kie.ai generation - Model: ${modelId}, Images: ${input.images?.length || 0}, Prompt: ${input.prompt.length} chars, Veo: ${isVeo}, FluxKontext: ${isFluxKontext}, Runway: ${isRunway}`);
// Build the input object (all parameters go inside "input" for Kie API)
// Start with model-specific required defaults
@ -1600,7 +1724,7 @@ async function generateWithKie(
}
}
// Build request body - Veo uses different format (no input wrapper, camelCase)
// Build request body - dedicated endpoints use top-level params (no input wrapper)
let requestBody: Record<string, unknown>;
if (isVeo) {
@ -1619,6 +1743,34 @@ async function generateWithKie(
} else if (inputParams.image_urls) {
requestBody.imageUrls = inputParams.image_urls;
}
} else if (isFluxKontext) {
// Flux Kontext format: top-level camelCase params, model name derived from ID
requestBody = {
model: modelId === "flux-kontext-max" ? "max" : "pro",
prompt: inputParams.prompt,
aspectRatio: inputParams.aspectRatio || inputParams.aspect_ratio || "1:1",
};
if (inputParams.outputFormat) requestBody.outputFormat = inputParams.outputFormat;
if (inputParams.safetyTolerance !== undefined) requestBody.safetyTolerance = inputParams.safetyTolerance;
if (inputParams.seed !== undefined) requestBody.seed = inputParams.seed;
// Singular image_url for Kontext
if (inputParams.image_url) {
requestBody.image_url = Array.isArray(inputParams.image_url) ? inputParams.image_url[0] : inputParams.image_url;
}
} else if (isRunway) {
// Runway format: top-level camelCase params
requestBody = {
model: modelId,
prompt: inputParams.prompt,
aspectRatio: inputParams.aspectRatio || inputParams.aspect_ratio || "16:9",
};
if (inputParams.duration !== undefined) requestBody.duration = inputParams.duration;
if (inputParams.quality) requestBody.quality = inputParams.quality;
if (inputParams.seed !== undefined) requestBody.seed = inputParams.seed;
// Singular imageUrl for Runway
if (inputParams.imageUrl) {
requestBody.imageUrl = Array.isArray(inputParams.imageUrl) ? inputParams.imageUrl[0] : inputParams.imageUrl;
}
} else {
// Standard format: { model, input: { ... } }
requestBody = {
@ -1628,9 +1780,16 @@ async function generateWithKie(
}
// 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";
let createUrl: string;
if (isVeo) {
createUrl = "https://api.kie.ai/api/v1/veo/generate";
} else if (isFluxKontext) {
createUrl = "https://api.kie.ai/api/v1/flux/kontext/generate";
} else if (isRunway) {
createUrl = "https://api.kie.ai/api/v1/runway/generate";
} else {
createUrl = "https://api.kie.ai/api/v1/jobs/createTask";
}
console.log(`[API:${requestId}] Calling Kie.ai API: ${createUrl}`);
// Log full request body for debugging (truncate very long prompts)

277
src/app/api/models/[modelId]/route.ts

@ -657,6 +657,283 @@ function getKieSchema(modelId: string): ExtractedSchema {
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
// --- New image models ---
"google/imagen4": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"google/imagen4-fast": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"google/imagen4-ultra": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"google/pro-image-to-image": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"flux-2/pro-text-to-image": {
parameters: [
...imageParams,
{ name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K"], default: "1K" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"flux-2/pro-image-to-image": {
parameters: [
...imageParams,
{ name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K"], default: "1K" },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"flux-2/flex-text-to-image": {
parameters: [
...imageParams,
{ name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K"], default: "1K" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"flux-2/flex-image-to-image": {
parameters: [
...imageParams,
{ name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K"], default: "1K" },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"flux-kontext-pro": {
parameters: [
{ name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], default: "1:1" },
{ name: "outputFormat", type: "string", description: "Output image format", enum: ["png", "jpeg", "webp"], default: "png" },
{ name: "safetyTolerance", type: "integer", description: "Safety tolerance level (0=strict, 6=permissive)", minimum: 0, maximum: 6, default: 2 },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_url", type: "image", required: false, label: "Image" },
],
},
"flux-kontext-max": {
parameters: [
{ name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], default: "1:1" },
{ name: "outputFormat", type: "string", description: "Output image format", enum: ["png", "jpeg", "webp"], default: "png" },
{ name: "safetyTolerance", type: "integer", description: "Safety tolerance level (0=strict, 6=permissive)", minimum: 0, maximum: 6, default: 2 },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_url", type: "image", required: false, label: "Image" },
],
},
"grok-imagine/text-to-image": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"grok-imagine/image-to-image": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"ideogram/v3-reframe": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"ideogram/character": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"ideogram/character-edit": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"qwen/text-to-image": {
parameters: imageParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"qwen/image-to-image": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"qwen/image-edit": {
parameters: imageParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
// --- New video models ---
"runway/gen4": {
parameters: [
{ name: "duration", type: "integer", description: "Video duration in seconds", minimum: 5, maximum: 10, default: 10 },
{ name: "quality", type: "string", description: "Output quality", enum: ["standard", "high"], default: "standard" },
{ name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "imageUrl", type: "image", required: false, label: "Image" },
],
},
"runway/aleph": {
parameters: [
{ name: "duration", type: "integer", description: "Video duration in seconds", minimum: 5, maximum: 10, default: 10 },
{ name: "quality", type: "string", description: "Output quality", enum: ["standard", "high"], default: "standard" },
{ name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16", "1:1"], default: "16:9" },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "imageUrl", type: "image", required: false, label: "Image" },
],
},
"grok-imagine/text-to-video": {
parameters: videoParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"grok-imagine/image-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"hailuo/2-3-image-to-video-pro": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"hailuo/2-3-image-to-video-standard": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"hailuo/02-text-to-video-pro": {
parameters: videoParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"hailuo/02-image-to-video-pro": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"bytedance/v1-pro-text-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: false, label: "Image", isArray: true },
],
},
"bytedance/v1-pro-image-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"bytedance/v1-pro-fast-image-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"bytedance/v1-lite-text-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "image_urls", type: "image", required: false, label: "Image", isArray: true },
],
},
"bytedance/v1-lite-image-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"kling/v2-1-master-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", enum: ["5", "10"], default: "5" },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"kling/v2-1-master-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", enum: ["5", "10"], default: "5" },
{ 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/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" },
{ name: "duration", type: "string", description: "Video duration", enum: ["5", "10"], default: "5" },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"kling/v2-5-turbo-image-to-video-pro": {
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", enum: ["5", "10"], default: "5" },
{ 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 },
],
},
"wan/2-2-a14b-text-to-video-turbo": {
parameters: videoParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
},
"wan/2-2-a14b-image-to-video-turbo": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
"wan/2-6-video-to-video": {
parameters: videoParams,
inputs: [
{ name: "prompt", type: "text", required: false, label: "Prompt" },
{ name: "image_urls", type: "image", required: true, label: "Video/Image", isArray: true },
],
},
};
return schemas[modelId] || { parameters: [], inputs: [] };

365
src/app/api/models/route.ts

@ -53,7 +53,7 @@ const RELEVANT_CATEGORIES = [
// Kie.ai models (hardcoded - no discovery API available)
const KIE_MODELS: ProviderModel[] = [
// Image Models
// ============ Image Models ============
{
id: "z-image",
name: "Z-Image",
@ -62,6 +62,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.004, currency: "USD" },
pageUrl: "https://kie.ai/z-image",
},
{
id: "seedream/4.5-text-to-image",
@ -71,6 +72,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.032, currency: "USD" },
pageUrl: "https://kie.ai/seedream",
},
{
id: "seedream/4.5-edit",
@ -80,6 +82,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.032, currency: "USD" },
pageUrl: "https://kie.ai/seedream",
},
{
id: "nano-banana-pro",
@ -89,6 +92,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-image", "image-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.10, currency: "USD" },
pageUrl: "https://kie.ai/nano-banana",
},
{
id: "gpt-image/1.5-text-to-image",
@ -98,6 +102,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.06, currency: "USD" },
pageUrl: "https://kie.ai/gpt-image-1",
},
{
id: "gpt-image/1.5-image-to-image",
@ -107,6 +112,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.06, currency: "USD" },
pageUrl: "https://kie.ai/gpt-image-1",
},
{
id: "google/nano-banana",
@ -116,6 +122,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.05, currency: "USD" },
pageUrl: "https://kie.ai/nano-banana",
},
{
id: "google/nano-banana-edit",
@ -125,8 +132,171 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.05, currency: "USD" },
pageUrl: "https://kie.ai/nano-banana",
},
// Video Models
{
id: "google/imagen4",
name: "Imagen 4",
description: "Google Imagen 4 text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/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://kie.ai/google/imagen4",
},
{
id: "google/imagen4-ultra",
name: "Imagen 4 Ultra",
description: "Google Imagen 4 Ultra high-quality text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/google/imagen4",
},
{
id: "google/pro-image-to-image",
name: "Gemini Pro Edit",
description: "Google Gemini Pro image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/nano-banana",
},
{
id: "flux-2/pro-text-to-image",
name: "FLUX.2 Pro",
description: "FLUX.2 Pro text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/flux-2",
},
{
id: "flux-2/pro-image-to-image",
name: "FLUX.2 Pro Edit",
description: "FLUX.2 Pro image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/flux-2",
},
{
id: "flux-2/flex-text-to-image",
name: "FLUX.2 Flex",
description: "FLUX.2 Flex text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/flux-2",
},
{
id: "flux-2/flex-image-to-image",
name: "FLUX.2 Flex Edit",
description: "FLUX.2 Flex image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/flux-2",
},
{
id: "flux-kontext-pro",
name: "Flux Kontext Pro",
description: "Flux Kontext Pro image generation and editing via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image", "image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/features/flux1-kontext",
},
{
id: "flux-kontext-max",
name: "Flux Kontext Max",
description: "Flux Kontext Max image generation and editing via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image", "image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/features/flux1-kontext",
},
{
id: "grok-imagine/text-to-image",
name: "Grok Imagine",
description: "Grok Imagine text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/grok-imagine",
},
{
id: "grok-imagine/image-to-image",
name: "Grok Imagine Edit",
description: "Grok Imagine image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/grok-imagine",
},
{
id: "ideogram/v3-reframe",
name: "Ideogram V3 Reframe",
description: "Ideogram V3 image reframing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/ideogram-v3",
},
{
id: "ideogram/character",
name: "Ideogram Character",
description: "Ideogram character-consistent text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/ideogram-v3",
},
{
id: "ideogram/character-edit",
name: "Ideogram Character Edit",
description: "Ideogram character-consistent image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/ideogram-v3",
},
{
id: "qwen/text-to-image",
name: "Qwen Image",
description: "Qwen text-to-image generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/qwen-image",
},
{
id: "qwen/image-to-image",
name: "Qwen Image Edit",
description: "Qwen image editing via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/qwen-image",
},
{
id: "qwen/image-edit",
name: "Qwen Image Inpaint",
description: "Qwen image inpainting via Kie.ai.",
provider: "kie",
capabilities: ["image-to-image"],
coverImage: undefined,
pageUrl: "https://kie.ai/qwen-image",
},
// ============ Video Models ============
{
id: "sora-2-text-to-video",
name: "Sora 2",
@ -135,6 +305,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video"],
coverImage: undefined,
pricing: { type: "per-second", amount: 0.015, currency: "USD" },
pageUrl: "https://kie.ai/sora",
},
{
id: "sora-2-image-to-video",
@ -144,6 +315,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-video"],
coverImage: undefined,
pricing: { type: "per-second", amount: 0.015, currency: "USD" },
pageUrl: "https://kie.ai/sora",
},
{
id: "sora-2-pro-text-to-video",
@ -153,6 +325,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video"],
coverImage: undefined,
pricing: { type: "per-second", amount: 0.10, currency: "USD" },
pageUrl: "https://kie.ai/sora",
},
{
id: "sora-2-pro-image-to-video",
@ -162,6 +335,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-video"],
coverImage: undefined,
pricing: { type: "per-second", amount: 0.10, currency: "USD" },
pageUrl: "https://kie.ai/sora",
},
{
id: "veo3_fast",
@ -171,6 +345,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video", "image-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.30, currency: "USD" },
pageUrl: "https://kie.ai/veo3",
},
{
id: "veo3",
@ -180,6 +355,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video", "image-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 1.25, currency: "USD" },
pageUrl: "https://kie.ai/veo3",
},
{
id: "bytedance/seedance-1.5-pro",
@ -189,6 +365,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video", "image-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.20, currency: "USD" },
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "kling-2.6/text-to-video",
@ -198,6 +375,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.60, currency: "USD" },
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "kling-2.6/image-to-video",
@ -207,6 +385,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.60, currency: "USD" },
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "wan/2-6-text-to-video",
@ -216,6 +395,7 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["text-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.90, currency: "USD" },
pageUrl: "https://kie.ai/wan-2-6",
},
{
id: "wan/2-6-image-to-video",
@ -225,6 +405,187 @@ const KIE_MODELS: ProviderModel[] = [
capabilities: ["image-to-video"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.90, currency: "USD" },
pageUrl: "https://kie.ai/wan-2-6",
},
{
id: "runway/gen4",
name: "Runway Gen-4",
description: "Runway Gen-4 video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video", "image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/runway/gen4-turbo",
},
{
id: "runway/aleph",
name: "Runway Aleph",
description: "Runway Aleph video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video", "image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/runway/gen4-aleph",
},
{
id: "grok-imagine/text-to-video",
name: "Grok Imagine Video",
description: "Grok Imagine text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/grok-imagine",
},
{
id: "grok-imagine/image-to-video",
name: "Grok Imagine I2V",
description: "Grok Imagine image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/grok-imagine",
},
{
id: "hailuo/2-3-image-to-video-pro",
name: "Hailuo 2.3 Pro",
description: "Hailuo 2.3 Pro image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/hailuo-2-3",
},
{
id: "hailuo/2-3-image-to-video-standard",
name: "Hailuo 2.3",
description: "Hailuo 2.3 standard image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/hailuo-2-3",
},
{
id: "hailuo/02-text-to-video-pro",
name: "Hailuo Pro",
description: "Hailuo Pro text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/hailuo-2-3",
},
{
id: "hailuo/02-image-to-video-pro",
name: "Hailuo Pro I2V",
description: "Hailuo Pro image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/hailuo-2-3",
},
{
id: "bytedance/v1-pro-text-to-video",
name: "Seedance Pro",
description: "ByteDance Seedance Pro text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "bytedance/v1-pro-image-to-video",
name: "Seedance Pro I2V",
description: "ByteDance Seedance Pro image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "bytedance/v1-pro-fast-image-to-video",
name: "Seedance Pro Fast",
description: "ByteDance Seedance Pro fast image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "bytedance/v1-lite-text-to-video",
name: "Seedance Lite",
description: "ByteDance Seedance Lite text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "bytedance/v1-lite-image-to-video",
name: "Seedance Lite I2V",
description: "ByteDance Seedance Lite image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/seedance-1-5-pro",
},
{
id: "kling/v2-1-master-text-to-video",
name: "Kling 2.1 Master",
description: "Kling 2.1 Master text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "kling/v2-1-master-image-to-video",
name: "Kling 2.1 Master I2V",
description: "Kling 2.1 Master image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "kling/v2-5-turbo-text-to-video-pro",
name: "Kling 2.5 Turbo",
description: "Kling 2.5 Turbo text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "kling/v2-5-turbo-image-to-video-pro",
name: "Kling 2.5 Turbo I2V",
description: "Kling 2.5 Turbo image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/kling-2-6",
},
{
id: "wan/2-2-a14b-text-to-video-turbo",
name: "Wan 2.2 Turbo",
description: "Wan 2.2 Turbo text-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["text-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/wan-2-6",
},
{
id: "wan/2-2-a14b-image-to-video-turbo",
name: "Wan 2.2 Turbo I2V",
description: "Wan 2.2 Turbo image-to-video generation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/wan-2-6",
},
{
id: "wan/2-6-video-to-video",
name: "Wan 2.6 V2V",
description: "Wan 2.6 video-to-video transformation via Kie.ai.",
provider: "kie",
capabilities: ["image-to-video"],
coverImage: undefined,
pageUrl: "https://kie.ai/wan-2-6",
},
];

98
src/components/modals/ModelSearchDialog.tsx

@ -8,9 +8,9 @@ import { useReactFlow } from "@xyflow/react";
import { ProviderType, RecentModel } from "@/types";
import { ProviderModel, ModelCapability } from "@/lib/providers/types";
// Provider icons
// Provider icons — all normalized to w-3.5 h-3.5 with viewBoxes cropped to fill consistently
const ReplicateIcon = () => (
<svg className="w-4 h-4" viewBox="0 0 1000 1000" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="0 0 1000 1000" fill="currentColor">
<polygon points="1000,427.6 1000,540.6 603.4,540.6 603.4,1000 477,1000 477,427.6" />
<polygon points="1000,213.8 1000,327 364.8,327 364.8,1000 238.4,1000 238.4,213.8" />
<polygon points="1000,0 1000,113.2 126.4,113.2 126.4,1000 0,1000 0,0" />
@ -18,19 +18,25 @@ const ReplicateIcon = () => (
);
const FalIcon = () => (
<svg className="w-4 h-4" viewBox="0 0 1855 1855" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="0 0 1855 1855" fill="currentColor">
<path fillRule="evenodd" clipRule="evenodd" d="M1181.65 78C1212.05 78 1236.42 101.947 1239.32 131.261C1265.25 392.744 1480.07 600.836 1750.02 625.948C1780.28 628.764 1805 652.366 1805 681.816V1174.18C1805 1203.63 1780.28 1227.24 1750.02 1230.05C1480.07 1255.16 1265.25 1463.26 1239.32 1724.74C1236.42 1754.05 1212.05 1778 1181.65 1778H673.354C642.951 1778 618.585 1754.05 615.678 1724.74C589.754 1463.26 374.927 1255.16 104.984 1230.05C74.7212 1227.24 50 1203.63 50 1174.18V681.816C50 652.366 74.7213 628.764 104.984 625.948C374.927 600.836 589.754 392.744 615.678 131.261C618.585 101.946 642.951 78 673.353 78H1181.65ZM402.377 926.561C402.377 1209.41 638.826 1438.71 930.501 1438.71C1222.18 1438.71 1458.63 1209.41 1458.63 926.561C1458.63 643.709 1222.18 414.412 930.501 414.412C638.826 414.412 402.377 643.709 402.377 926.561Z" />
</svg>
);
const GeminiIcon = () => (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" />
</svg>
);
const KieIcon = () => (
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 3h3.5v7L17 3h4l-8 8.5L21 21h-4l-7.5-8.5V21H6V3z" />
</svg>
);
const WaveSpeedIcon = () => (
<svg className="w-4 h-4" viewBox="0 0 512 512" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="95 140 350 230" fill="currentColor">
<path d="M308.946 153.758C314.185 153.758 318.268 158.321 317.516 163.506C306.856 237.02 270.334 302.155 217.471 349.386C211.398 354.812 203.458 357.586 195.315 357.586H127.562C117.863 357.586 110.001 349.724 110.001 340.025V333.552C110.001 326.82 113.882 320.731 119.792 317.505C176.087 286.779 217.883 232.832 232.32 168.537C234.216 160.09 241.509 153.758 250.167 153.758H308.946Z" />
<path d="M183.573 153.758C188.576 153.758 192.592 157.94 192.069 162.916C187.11 210.12 160.549 250.886 122.45 275.151C116.916 278.676 110 274.489 110 267.928V171.318C110 161.62 117.862 153.758 127.56 153.758H183.573Z" />
<path d="M414.815 153.758C425.503 153.758 433.734 163.232 431.799 173.743C420.697 234.038 398.943 290.601 368.564 341.414C362.464 351.617 351.307 357.586 339.419 357.586H274.228C266.726 357.586 262.611 348.727 267.233 342.819C306.591 292.513 334.86 233.113 348.361 168.295C350.104 159.925 357.372 153.758 365.922 153.758H414.815Z" />
@ -85,7 +91,7 @@ export function ModelSearchDialog({
trackModelUsage,
} = useWorkflowStore();
// Use stable selector for API keys to prevent unnecessary re-fetches
const { replicateApiKey, falApiKey } = useProviderApiKeys();
const { replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey } = useProviderApiKeys();
const { screenToFlowPosition } = useReactFlow();
// State
@ -161,6 +167,12 @@ export function ModelSearchDialog({
if (falApiKey) {
headers["X-Fal-Key"] = falApiKey;
}
if (kieApiKey) {
headers["X-Kie-Key"] = kieApiKey;
}
if (wavespeedApiKey) {
headers["X-WaveSpeed-Key"] = wavespeedApiKey;
}
const response = await deduplicatedFetch(`/api/models?${params.toString()}`, {
headers,
@ -192,7 +204,7 @@ export function ModelSearchDialog({
setIsLoading(false);
}
}
}, [debouncedSearch, providerFilter, capabilityFilter, replicateApiKey, falApiKey]);
}, [debouncedSearch, providerFilter, capabilityFilter, replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey]);
// Fetch models when filters change
useEffect(() => {
@ -358,13 +370,18 @@ export function ModelSearchDialog({
};
// Get model page URL for the provider's website
const getModelUrl = (model: ProviderModel): string => {
if (model.provider === "replicate") {
return `https://replicate.com/${model.id}`;
} else if (model.provider === "fal") {
return `https://fal.ai/models/${model.id}`;
const getModelUrl = (model: ProviderModel): string | null => {
if (model.pageUrl) return model.pageUrl;
switch (model.provider) {
case "replicate":
return `https://replicate.com/${model.id}`;
case "fal":
return `https://fal.ai/models/${model.id}`;
case "wavespeed":
return `https://wavespeed.ai`;
default:
return null;
}
return "#";
};
// Get capability badges - show all capabilities to differentiate similar models
@ -516,6 +533,17 @@ export function ModelSearchDialog({
>
<FalIcon />
</button>
<button
onClick={() => setProviderFilter("kie")}
title="Kie.ai"
className={`p-2 rounded transition-colors ${
providerFilter === "kie"
? "bg-orange-500/20 text-orange-300"
: "text-neutral-400 hover:text-orange-300 hover:bg-neutral-700"
}`}
>
<KieIcon />
</button>
<button
onClick={() => setProviderFilter("wavespeed")}
title="WaveSpeed"
@ -739,28 +767,30 @@ export function ModelSearchDialog({
<span className="text-xs text-neutral-500 truncate font-mono">
{model.id}
</span>
<a
href={getModelUrl(model)}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-neutral-500 hover:text-neutral-300 transition-colors flex-shrink-0"
title={`View on ${getProviderDisplayName(model.provider)}`}
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
{getModelUrl(model) && (
<a
href={getModelUrl(model)!}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-neutral-500 hover:text-neutral-300 transition-colors flex-shrink-0"
title={`View on ${getProviderDisplayName(model.provider)}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
)}
</div>
{/* Badges row */}

2
src/lib/providers/types.ts

@ -72,6 +72,8 @@ export interface ProviderModel {
amount: number;
currency: string;
};
/** Optional URL to the model's page on the provider's website */
pageUrl?: string;
}
/**

Loading…
Cancel
Save