/** * Unified Models API Endpoint * * Aggregates models from all configured providers (Replicate, fal.ai, Gemini, WaveSpeed). * Uses in-memory caching to reduce external API calls. * * GET /api/models * * Query params: * - provider: Optional, filter to specific provider ("replicate" | "fal" | "gemini" | "wavespeed") * - search: Optional, search query * - refresh: Optional, bypass cache if "true" * - capabilities: Optional, filter by capabilities (comma-separated) * * Headers: * - X-NewApiWG-Base-URL: NewApiWG OpenAI-compatible base URL * - X-Replicate-Key: Replicate API key * - X-Fal-Key: fal.ai API key (optional, works without but rate limited) * - X-WaveSpeed-Key: WaveSpeed API key * * Response: * { * success: true, * models: ProviderModel[], * cached: boolean, * providers: { [provider]: { success, count, cached?, error? } }, * errors?: string[] * } */ import { NextRequest, NextResponse } from "next/server"; import { ProviderType } from "@/types"; import { ProviderModel, ModelCapability } from "@/lib/providers"; import { getCachedModels, setCachedModels, getCacheKey, setCachedWaveSpeedSchemas, WaveSpeedApiSchema, } from "@/lib/providers/cache"; import { fetchNewApiWGModels, normalizeNewApiWGBaseUrl } from "@/app/api/generate/providers/newapiwg"; import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID, } from "@/lib/providerMode"; import { requireLogin } from "@/app/api/_auth"; import { apiErrorResponse } from "@/app/api/_errors"; import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey"; // API base URLs const REPLICATE_API_BASE = "https://api.replicate.com/v1"; const FAL_API_BASE = "https://api.fal.ai/v1"; const WAVESPEED_API_BASE = "https://api.wavespeed.ai/api/v3"; // Categories we care about for image/video/3D/audio generation (fal.ai) const RELEVANT_CATEGORIES = [ "text-to-image", "image-to-image", "text-to-video", "image-to-video", "text-to-3d", "image-to-3d", "text-to-speech", "text-to-music", "text-to-sound-effects", "audio-to-video", ]; // Kie.ai models (hardcoded - no discovery API available) const KIE_MODELS: ProviderModel[] = [ // ============ Image Models (11) ============ { id: "z-image", name: "Z-Image", description: "Fast, affordable text-to-image generation. Great for quick iterations.", provider: "kie", 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", name: "Seedream 4.5", description: "High-quality text-to-image generation with excellent prompt following.", provider: "kie", 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", name: "Seedream 4.5 Edit", description: "Image editing and transformation using Seedream 4.5.", provider: "kie", capabilities: ["image-to-image"], coverImage: undefined, pricing: { type: "per-run", amount: 0.032, currency: "USD" }, pageUrl: "https://kie.ai/seedream", }, { id: "gpt-image/1.5-text-to-image", name: "GPT Image 1.5", description: "OpenAI-style image generation with excellent prompt understanding.", provider: "kie", 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", name: "GPT Image 1.5 Edit", description: "Image editing using GPT Image 1.5 model.", provider: "kie", capabilities: ["image-to-image"], coverImage: undefined, pricing: { type: "per-run", amount: 0.06, currency: "USD" }, pageUrl: "https://kie.ai/gpt-image-1", }, { 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: "nano-banana-pro", name: "Nano Banana Pro", description: "Google Gemini 3 Pro image generation via Kie.ai. Supports text-to-image and image-to-image with up to 8 input images.", provider: "kie", capabilities: ["text-to-image", "image-to-image"], 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/nanobanana2", }, { 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", 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", }, // ============ 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", 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: "kling-2.6/text-to-video", name: "Kling 2.6", description: "Kling 2.6 video generation from text.", provider: "kie", 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", name: "Kling 2.6 Image-to-Video", description: "Kling 2.6 video generation from images.", provider: "kie", capabilities: ["image-to-video"], coverImage: undefined, pricing: { type: "per-run", amount: 0.60, currency: "USD" }, pageUrl: "https://kie.ai/kling-2-6", }, { id: "kling-2.6/motion-control", name: "Kling 2.6 Motion Control", description: "Motion transfer from video to static image. Supports 720p and 1080p output.", provider: "kie", capabilities: ["image-to-video"], 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", 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-6-text-to-video", name: "Wan 2.6", description: "Wan 2.6 video generation from text.", provider: "kie", 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", name: "Wan 2.6 Image-to-Video", description: "Wan 2.6 video generation from images.", provider: "kie", capabilities: ["image-to-video"], coverImage: undefined, pricing: { type: "per-run", amount: 0.90, currency: "USD" }, 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", }, { 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", description: "AI video upscaling. Supports 1x, 2x, and 4x scaling factors.", provider: "kie", capabilities: ["image-to-video"], coverImage: undefined, pageUrl: "https://kie.ai/topaz", }, { id: "veo3/text-to-video", name: "Veo 3", description: "Google Veo 3.1 high-quality text-to-video generation with audio via Kie.ai.", provider: "kie", capabilities: ["text-to-video"], coverImage: undefined, pageUrl: "https://docs.kie.ai/veo3-api/quickstart", }, { id: "veo3/image-to-video", name: "Veo 3 I2V", description: "Google Veo 3.1 image-to-video generation via Kie.ai. Supports 1-2 reference images.", provider: "kie", capabilities: ["image-to-video"], coverImage: undefined, pageUrl: "https://docs.kie.ai/veo3-api/quickstart", }, { id: "veo3-fast/text-to-video", name: "Veo 3 Fast", description: "Google Veo 3.1 fast text-to-video generation with audio via Kie.ai.", provider: "kie", capabilities: ["text-to-video"], coverImage: undefined, pageUrl: "https://docs.kie.ai/veo3-api/quickstart", }, { id: "veo3-fast/image-to-video", name: "Veo 3 Fast I2V", description: "Google Veo 3.1 fast image-to-video generation via Kie.ai. Supports 1-2 reference images.", provider: "kie", capabilities: ["image-to-video"], coverImage: undefined, pageUrl: "https://docs.kie.ai/veo3-api/quickstart", }, // ============ Audio/TTS Models (4) ============ { id: "elevenlabs/turbo-v2.5", name: "ElevenLabs Turbo v2.5", description: "Fast, high-quality text-to-speech with natural-sounding voices from ElevenLabs via Kie.ai.", provider: "kie", capabilities: ["text-to-audio"], coverImage: undefined, pricing: { type: "per-run", amount: 0.05, currency: "USD" }, pageUrl: "https://kie.ai/elevenlabs-tts", }, { id: "elevenlabs/multilingual-v2", name: "ElevenLabs Multilingual v2", description: "Multilingual text-to-speech supporting multiple languages with natural voices via Kie.ai.", provider: "kie", capabilities: ["text-to-audio"], coverImage: undefined, pricing: { type: "per-run", amount: 0.05, currency: "USD" }, pageUrl: "https://kie.ai/elevenlabs-tts", }, { id: "elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Eleven V3", description: "ElevenLabs' most expressive text-to-speech model with emotional nuance, supporting 70+ languages and audio tags for dialogue via Kie.ai.", provider: "kie", capabilities: ["text-to-audio"], coverImage: undefined, pricing: { type: "per-run", amount: 0.06, currency: "USD" }, pageUrl: "https://kie.ai/elevenlabs/text-to-dialogue-v3", }, { id: "elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effects v2", description: "Generate sound effects from text descriptions. Supports looping, 0.5-22 second duration, and multiple output formats via Kie.ai.", provider: "kie", capabilities: ["text-to-audio"], coverImage: undefined, pricing: { type: "per-run", amount: 0.02, currency: "USD" }, pageUrl: "https://kie.ai/elevenlabs-sound-effect", }, ]; // Gemini image models (hardcoded - these don't come from an external API) const GEMINI_IMAGE_MODELS: ProviderModel[] = [ { id: "nano-banana", name: "Nano Banana", description: "Fast image generation with Gemini 2.5 Flash. Supports text-to-image and image-to-image with aspect ratio control.", provider: "gemini", capabilities: ["text-to-image", "image-to-image"], coverImage: undefined, pricing: { type: "per-run", amount: 0.039, currency: "USD" }, }, { id: "nano-banana-2", name: "Nano Banana 2", description: "High-efficiency image generation with Gemini 3.1 Flash. Supports resolution control (512/1K/2K/4K), Google Search grounding, and up to 10 reference images.", provider: "gemini", capabilities: ["text-to-image", "image-to-image"], coverImage: undefined, pricing: { type: "per-run", amount: 0.067, currency: "USD" }, }, { id: "nano-banana-pro", name: "Nano Banana Pro", description: "High-quality image generation with Gemini 3 Pro. Supports text-to-image, image-to-image, resolution control (1K/2K/4K), and Google Search grounding.", provider: "gemini", capabilities: ["text-to-image", "image-to-image"], coverImage: undefined, pricing: { type: "per-run", amount: 0.134, currency: "USD" }, }, ]; // Gemini video models (native Veo via Gemini API) const GEMINI_VIDEO_MODELS: ProviderModel[] = [ { id: "veo-3.1/text-to-video", name: "Veo 3.1", description: "Highest quality video generation with Veo 3.1. Supports 720p/1080p/4k, 4-8 second clips, and native audio via Gemini API.", provider: "gemini", capabilities: ["text-to-video"], coverImage: undefined, pricing: { type: "per-second", amount: 0.40, currency: "USD" }, }, { id: "veo-3.1/image-to-video", name: "Veo 3.1 I2V", description: "Image-to-video generation with Veo 3.1. Supports 720p/1080p/4k, 4-8 second clips, and native audio via Gemini API.", provider: "gemini", capabilities: ["image-to-video"], coverImage: undefined, pricing: { type: "per-second", amount: 0.40, currency: "USD" }, }, { id: "veo-3.1-fast/text-to-video", name: "Veo 3.1 Fast", description: "Fast, cost-effective video generation with Veo 3.1 Fast. Supports 720p/1080p/4k, 4-8 second clips via Gemini API.", provider: "gemini", capabilities: ["text-to-video"], coverImage: undefined, pricing: { type: "per-second", amount: 0.15, currency: "USD" }, }, { id: "veo-3.1-fast/image-to-video", name: "Veo 3.1 Fast I2V", description: "Fast image-to-video generation with Veo 3.1 Fast. Supports 720p/1080p/4k, 4-8 second clips via Gemini API.", provider: "gemini", capabilities: ["image-to-video"], coverImage: undefined, pricing: { type: "per-second", amount: 0.15, currency: "USD" }, }, ]; // WaveSpeed models are now fetched dynamically from https://api.wavespeed.ai/api/v3/models // ============ Replicate Types ============ interface ReplicateModelsResponse { next: string | null; previous: string | null; results: ReplicateModel[]; } interface ReplicateModel { url: string; owner: string; name: string; description: string | null; visibility: "public" | "private"; github_url?: string; paper_url?: string; license_url?: string; run_count: number; cover_image_url?: string; default_example?: Record; latest_version?: { id: string; openapi_schema?: Record; }; } // ============ Fal.ai Types ============ interface FalModelsResponse { models: FalModel[]; next_cursor: string | null; has_more: boolean; } interface FalModel { endpoint_id: string; metadata: { display_name: string; category: string; description: string; status: "active" | "deprecated"; tags: string[]; updated_at: string; is_favorited: boolean | null; thumbnail_url: string; model_url: string; date: string; highlighted: boolean; pinned: boolean; thumbnail_animated_url?: string; github_url?: string; license_type?: "commercial" | "research" | "private"; }; openapi?: Record; } // ============ Response Types ============ interface ProviderResult { success: boolean; count: number; cached?: boolean; error?: string; } interface ModelsSuccessResponse { success: true; models: ProviderModel[]; cached: boolean; providers: Record; /** All providers that have API keys configured (env or client header) */ availableProviders: string[]; errors?: string[]; } interface ModelsErrorResponse { success: false; error: string; } type ModelsResponse = ModelsSuccessResponse | ModelsErrorResponse; // ============ Replicate Helpers ============ function inferReplicateCapabilities(model: ReplicateModel): ModelCapability[] { const capabilities: ModelCapability[] = []; const searchText = `${model.name} ${model.description ?? ""}`.toLowerCase(); // Check for 3D-related keywords first const is3DModel = searchText.includes("3d") || searchText.includes("mesh") || searchText.includes("triposr") || searchText.includes("tripo") || searchText.includes("hunyuan3d") || searchText.includes("instant-mesh") || searchText.includes("point-e") || searchText.includes("shap-e"); if (is3DModel) { // 3D model - determine if image-to-3d or text-to-3d const hasImageInput = searchText.includes("image") || searchText.includes("img") || searchText.includes("photo"); if (hasImageInput) { capabilities.push("image-to-3d"); } else { capabilities.push("text-to-3d"); } return capabilities; } // Check for audio-related keywords const isAudioModel = searchText.includes("music") || searchText.includes("audio") || searchText.includes("tts") || searchText.includes("text-to-speech") || searchText.includes("speech") || searchText.includes("sound effect") || searchText.includes("voice") || searchText.includes("bark") || searchText.includes("xtts"); if (isAudioModel) { capabilities.push("text-to-audio"); return capabilities; } // Check for video-related keywords const isVideoModel = searchText.includes("video") || searchText.includes("animate") || searchText.includes("motion") || searchText.includes("luma") || searchText.includes("kling") || searchText.includes("minimax"); if (isVideoModel) { // Video model - determine video capability type if ( searchText.includes("img2vid") || searchText.includes("image-to-video") || searchText.includes("i2v") ) { capabilities.push("image-to-video"); } else { capabilities.push("text-to-video"); } } else { // Image model - default to text-to-image capabilities.push("text-to-image"); // Check for image-to-image capability if ( searchText.includes("img2img") || searchText.includes("image-to-image") || searchText.includes("inpaint") || searchText.includes("controlnet") || searchText.includes("upscale") || searchText.includes("restore") ) { capabilities.push("image-to-image"); } } return capabilities; } function mapReplicateModel(model: ReplicateModel): ProviderModel { return { id: `${model.owner}/${model.name}`, name: model.name, description: model.description, provider: "replicate", capabilities: inferReplicateCapabilities(model), coverImage: model.cover_image_url, }; } async function fetchReplicateModels(apiKey: string): Promise { const allModels: ProviderModel[] = []; // Always fetch from the models endpoint - search endpoint is unreliable let url: string | null = `${REPLICATE_API_BASE}/models`; // Paginate through results (limit to 15 pages to avoid timeout) let pageCount = 0; const maxPages = 15; while (url && pageCount < maxPages) { const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}`, }, }); if (!response.ok) { throw new Error(`Replicate API error: ${response.status}`); } const data: ReplicateModelsResponse = await response.json(); if (data.results) { allModels.push(...data.results.map(mapReplicateModel)); } url = data.next; pageCount++; } return allModels; } /** * Filter models by search query (client-side filtering for Replicate) */ function filterModelsBySearch( models: ProviderModel[], searchQuery: string ): ProviderModel[] { const searchLower = searchQuery.toLowerCase(); return models.filter((model) => { const nameMatch = model.name.toLowerCase().includes(searchLower); const descMatch = model.description?.toLowerCase().includes(searchLower) || false; const idMatch = model.id.toLowerCase().includes(searchLower); return nameMatch || descMatch || idMatch; }); } // ============ WaveSpeed Types ============ interface WaveSpeedModel { // Model ID can be in different fields depending on API version model_id?: string; id?: string; modelId?: string; name?: string; display_name?: string; description?: string; category?: string; type?: string; thumbnail_url?: string; cover_image?: string; coverImage?: string; pricing?: { amount?: number; currency?: string; }; // Dynamic schema from API (contains api_schemas[] with request_schema) api_schema?: WaveSpeedApiSchema; } interface WaveSpeedModelsResponse { models?: WaveSpeedModel[]; data?: WaveSpeedModel[]; results?: WaveSpeedModel[]; } // ============ WaveSpeed Helpers ============ function inferWaveSpeedCapabilities(model: WaveSpeedModel): ModelCapability[] { const capabilities: ModelCapability[] = []; const modelId = model.model_id?.toLowerCase() || ""; const name = (model.name || model.display_name || "").toLowerCase(); const description = (model.description || "").toLowerCase(); const category = (model.category || model.type || "").toLowerCase(); const searchText = `${modelId} ${name} ${description} ${category}`; // Check for 3D-related keywords first const is3DModel = searchText.includes("3d") || searchText.includes("mesh") || searchText.includes("tripo") || searchText.includes("hunyuan3d") || category.includes("3d"); if (is3DModel) { const hasImageInput = searchText.includes("image") || searchText.includes("img") || searchText.includes("photo"); if (hasImageInput) { capabilities.push("image-to-3d"); } else { capabilities.push("text-to-3d"); } return capabilities; } // Check for audio-related keywords const isAudioModel = searchText.includes("music") || searchText.includes("audio") || searchText.includes("tts") || searchText.includes("text-to-speech") || searchText.includes("speech") || searchText.includes("sound effect") || searchText.includes("voice") || category.includes("audio") || category.includes("music") || category.includes("speech"); if (isAudioModel) { capabilities.push("text-to-audio"); return capabilities; } // Check for video-related keywords const isVideoModel = searchText.includes("video") || searchText.includes("animate") || searchText.includes("motion") || searchText.includes("wan") || searchText.includes("kling") || searchText.includes("luma") || searchText.includes("minimax") || searchText.includes("i2v") || searchText.includes("t2v") || category.includes("video"); if (isVideoModel) { if ( searchText.includes("img2vid") || searchText.includes("image-to-video") || searchText.includes("i2v") ) { capabilities.push("image-to-video"); } else { capabilities.push("text-to-video"); } } else { // Image model capabilities.push("text-to-image"); // Check for image-to-image capability if ( searchText.includes("img2img") || searchText.includes("image-to-image") || searchText.includes("inpaint") || searchText.includes("controlnet") || searchText.includes("upscale") || searchText.includes("edit") || searchText.includes("kontext") ) { capabilities.push("image-to-image"); } } return capabilities.length > 0 ? capabilities : ["text-to-image"]; } function mapWaveSpeedModel(model: WaveSpeedModel): ProviderModel { // Handle different field names for model ID const modelId = model.model_id || model.id || model.modelId || model.name || "unknown"; const displayName = model.display_name || model.name || modelId; return { id: modelId, name: displayName, description: model.description || null, provider: "wavespeed", capabilities: inferWaveSpeedCapabilities(model), coverImage: model.thumbnail_url || model.cover_image || model.coverImage, pricing: model.pricing ? { type: "per-run", amount: model.pricing.amount || 0, currency: model.pricing.currency || "USD", } : undefined, }; } async function fetchWaveSpeedModels(apiKey: string): Promise { const response = await fetch(`${WAVESPEED_API_BASE}/models`, { headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, }); if (!response.ok) { throw new Error(`WaveSpeed API error: ${response.status}`); } const data: WaveSpeedModelsResponse = await response.json(); // Handle different response formats (models, data, or results array) const models = data.models || data.data || data.results || []; if (!Array.isArray(models)) { console.warn("[WaveSpeed] Unexpected response format:", data); return []; } // Log first model structure for debugging (including api_schema if present) if (models.length > 0) { const firstModel = models[0]; console.log("[WaveSpeed] First model sample:", JSON.stringify(firstModel, null, 2).substring(0, 1000)); console.log(`[WaveSpeed] Total models: ${models.length}`); console.log(`[WaveSpeed] First model has api_schema: ${!!firstModel.api_schema}`); } // Extract and cache schemas from models that have them const schemaMap = new Map(); for (const model of models) { const modelId = model.model_id || model.id || model.modelId || model.name; if (modelId && model.api_schema) { schemaMap.set(modelId, model.api_schema); } } // Bulk cache all schemas if (schemaMap.size > 0) { console.log(`[WaveSpeed] Caching ${schemaMap.size} model schemas`); setCachedWaveSpeedSchemas(schemaMap); } return models.map(mapWaveSpeedModel); } // ============ Fal.ai Helpers ============ const FAL_AUDIO_CATEGORIES: Record = { "text-to-speech": "text-to-audio", "text-to-music": "text-to-audio", "text-to-sound-effects": "text-to-audio", }; function mapFalCategory(category: string): ModelCapability | null { if (category in FAL_AUDIO_CATEGORIES) { return FAL_AUDIO_CATEGORIES[category]; } if (RELEVANT_CATEGORIES.includes(category)) { return category as ModelCapability; } return null; } function isRelevantFalModel(model: FalModel): boolean { return RELEVANT_CATEGORIES.includes(model.metadata.category); } function mapFalModel(model: FalModel): ProviderModel { const capability = mapFalCategory(model.metadata.category); return { id: model.endpoint_id, name: model.metadata.display_name, description: model.metadata.description, provider: "fal", capabilities: capability ? [capability] : [], coverImage: model.metadata.thumbnail_url, }; } async function fetchFalModels( apiKey: string | null, searchQuery?: string ): Promise { const allModels: ProviderModel[] = []; let cursor: string | null = null; let hasMore = true; const headers: HeadersInit = {}; if (apiKey) { headers["Authorization"] = `Key ${apiKey}`; } // Paginate through results (limit to 15 pages to avoid timeout) let pageCount = 0; const maxPages = 15; while (hasMore && pageCount < maxPages) { let url = `${FAL_API_BASE}/models?status=active`; if (searchQuery) { url += `&q=${encodeURIComponent(searchQuery)}`; } if (cursor) { url += `&cursor=${encodeURIComponent(cursor)}`; } const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`fal.ai API error: ${response.status}`); } const data: FalModelsResponse = await response.json(); allModels.push(...data.models.filter(isRelevantFalModel).map(mapFalModel)); cursor = data.next_cursor; hasMore = data.has_more; pageCount++; } // Note: Pricing not fetched - external provider pricing is unreliable // CostDialog shows model links instead of prices for fal.ai/Replicate return allModels; } // ============ Main Handler ============ export async function GET( request: NextRequest ): Promise> { let loginToken: string; try { loginToken = requireLogin(request).token; } catch (error) { return apiErrorResponse(error) as unknown as NextResponse; } // Parse query params const providerFilter = request.nextUrl.searchParams.get("provider") as | ProviderType | null; const searchQuery = request.nextUrl.searchParams.get("search") || undefined; const refresh = request.nextUrl.searchParams.get("refresh") === "true"; const capabilitiesParam = request.nextUrl.searchParams.get("capabilities"); const capabilitiesFilter: ModelCapability[] | null = capabilitiesParam ? (capabilitiesParam.split(",") as ModelCapability[]) : null; // Get API keys from headers, falling back to env variables const replicateKey = request.headers.get("X-Replicate-Key") || process.env.REPLICATE_API_KEY || null; const falKey = request.headers.get("X-Fal-Key") || process.env.FAL_API_KEY || null; const kieKey = request.headers.get("X-Kie-Key") || process.env.KIE_API_KEY || null; const wavespeedKey = request.headers.get("X-WaveSpeed-Key") || process.env.WAVESPEED_API_KEY || null; const newApiWGBaseUrl = normalizeNewApiWGBaseUrl(request.headers.get("X-NewApiWG-Base-URL")); const popiProviderMode = isPopiProviderMode(); const needsNewApiWGKey = popiProviderMode || !providerFilter || providerFilter === "newapiwg"; let newApiWGKey: string | null = null; if (needsNewApiWGKey) { try { newApiWGKey = await resolveUserGatewayApiKey(request); } catch (error) { if (popiProviderMode || providerFilter === "newapiwg") { return apiErrorResponse(error) as unknown as NextResponse; } } } if (popiProviderMode && providerFilter && providerFilter !== POPI_PROVIDER_ID) { return NextResponse.json( { success: false, error: `${getProviderDisplayName(providerFilter)} is disabled in Popi provider mode.`, }, { status: 400 } ); } // Build list of all available providers (have keys from env or client headers) const availableProviders: string[] = popiProviderMode ? newApiWGKey ? [POPI_PROVIDER_ID] : [] : []; if (!popiProviderMode) { if (newApiWGKey) availableProviders.push("newapiwg"); availableProviders.push("gemini"); // Gemini always available if (falKey) availableProviders.push("fal"); if (replicateKey) availableProviders.push("replicate"); if (kieKey) availableProviders.push("kie"); if (wavespeedKey) availableProviders.push("wavespeed"); } // Determine which providers to fetch from (excluding gemini/kie - handled separately as hardcoded) const providersToFetch: ProviderType[] = []; let includeGemini = false; let includeKie = false; if (popiProviderMode) { if (!newApiWGKey) { return NextResponse.json( { success: false, error: "Current user gateway API key is required.", }, { status: 400 } ); } providersToFetch.push(POPI_PROVIDER_ID); } else if (providerFilter) { if (providerFilter === "newapiwg") { if (newApiWGKey) { providersToFetch.push("newapiwg"); } else { return NextResponse.json( { success: false, error: "Current user gateway API key is required.", }, { status: 400 } ); } } else if (providerFilter === "gemini") { // Only Gemini requested - no external API calls needed includeGemini = true; } else if (providerFilter === "kie") { // Only Kie requested - no external API calls needed (hardcoded models) if (kieKey) { includeKie = true; } else { return NextResponse.json( { success: false, error: "Kie API key required. Add KIE_API_KEY to .env.local or configure in Settings.", }, { status: 400 } ); } } else if (providerFilter === "wavespeed") { if (wavespeedKey) { // WaveSpeed requested with key - fetch from API providersToFetch.push("wavespeed"); } else { // WaveSpeed requested but no key configured return NextResponse.json( { success: false, error: "WaveSpeed API key required. Add WAVESPEED_API_KEY to .env.local or configure in Settings.", }, { status: 400 } ); } } else if (providerFilter === "replicate" && replicateKey) { providersToFetch.push("replicate"); } else if (providerFilter === "fal" && falKey) { providersToFetch.push("fal"); } } else { // Include all providers that have keys configured if (newApiWGKey) { providersToFetch.push("newapiwg"); } includeGemini = !newApiWGKey; // Prefer the unified gateway when configured includeKie = kieKey ? true : false; // Kie only if API key is configured if (wavespeedKey) { providersToFetch.push("wavespeed"); // WaveSpeed if key is configured } if (replicateKey) { providersToFetch.push("replicate"); } if (falKey) { providersToFetch.push("fal"); } } // Gemini and Kie are always available (with key for Kie), so we don't fail if no external providers if (providersToFetch.length === 0 && !includeGemini && !includeKie) { return NextResponse.json( { success: false, error: "No providers available. Current user gateway API key is required.", }, { status: 400 } ); } const allModels: ProviderModel[] = []; const providerResults: Record = {}; const errors: string[] = []; let anyFromCache = false; let allFromCache = true; // Add Gemini models first if included (they appear at the top) if (includeGemini) { // Filter by search query if provided let geminiModels = [...GEMINI_IMAGE_MODELS, ...GEMINI_VIDEO_MODELS]; if (searchQuery) { geminiModels = filterModelsBySearch(geminiModels, searchQuery); } allModels.push(...geminiModels); providerResults["gemini"] = { success: true, count: geminiModels.length, cached: true, // Hardcoded models are effectively "cached" }; anyFromCache = true; } // Add Kie models if included (hardcoded, no API call needed) if (includeKie) { // Filter by search query if provided let kieModels = KIE_MODELS; if (searchQuery) { kieModels = filterModelsBySearch(kieModels, searchQuery); } allModels.push(...kieModels); providerResults["kie"] = { success: true, count: kieModels.length, cached: true, // Hardcoded models are effectively "cached" }; anyFromCache = true; } // Fetch from each provider (replicate, fal, wavespeed) for (const provider of providersToFetch) { // For Replicate and WaveSpeed, always use base cache key since we filter client-side // For fal.ai, include search in cache key since their API supports search const cacheKey = provider === "newapiwg" ? `${getCacheKey(provider)}:cap-v2:${newApiWGBaseUrl}:user:${loginToken.slice(-8)}` : provider === "replicate" || provider === "wavespeed" ? getCacheKey(provider) : getCacheKey(provider, searchQuery); let models: ProviderModel[] | null = null; let fromCache = false; // Check cache first (unless refresh=true) if (!refresh) { const cached = getCachedModels(cacheKey); if (cached) { models = cached; fromCache = true; anyFromCache = true; // For Replicate and WaveSpeed, apply client-side search filtering on cached models if ((provider === "replicate" || provider === "wavespeed" || provider === "newapiwg") && searchQuery) { models = filterModelsBySearch(models, searchQuery); } } } // Fetch from API if cache miss if (!models) { allFromCache = false; try { if (provider === "newapiwg") { const allNewApiWGModels = await fetchNewApiWGModels(newApiWGKey!, newApiWGBaseUrl, undefined, loginToken); setCachedModels(cacheKey, allNewApiWGModels); models = searchQuery ? filterModelsBySearch(allNewApiWGModels, searchQuery) : allNewApiWGModels; } else if (provider === "replicate") { // Fetch all models (no search param - we filter client-side) const allReplicateModels = await fetchReplicateModels(replicateKey!); // Cache the full list setCachedModels(cacheKey, allReplicateModels); // Apply search filter if needed models = searchQuery ? filterModelsBySearch(allReplicateModels, searchQuery) : allReplicateModels; } else if (provider === "fal") { models = await fetchFalModels(falKey, searchQuery); // Cache the results (fal.ai handles search server-side) setCachedModels(cacheKey, models); } else if (provider === "wavespeed") { // Fetch all models from WaveSpeed API const allWaveSpeedModels = await fetchWaveSpeedModels(wavespeedKey!); // Cache the full list setCachedModels(cacheKey, allWaveSpeedModels); // Apply search filter if needed (client-side filtering like Replicate) models = searchQuery ? filterModelsBySearch(allWaveSpeedModels, searchQuery) : allWaveSpeedModels; } else { models = []; } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.error(`[Models] ${provider}: ${errorMessage}`); errors.push(`${provider}: ${errorMessage}`); providerResults[provider] = { success: false, count: 0, error: errorMessage, }; continue; } } // Add to results allModels.push(...models); providerResults[provider] = { success: true, count: models.length, cached: fromCache, }; } // Check if we got any models if (allModels.length === 0 && errors.length === providersToFetch.length) { // All providers failed return NextResponse.json( { success: false, error: `All providers failed: ${errors.join("; ")}`, }, { status: 500 } ); } // Filter by capabilities if specified let filteredModels = allModels; if (capabilitiesFilter && capabilitiesFilter.length > 0) { filteredModels = allModels.filter((model) => model.capabilities.some((cap) => capabilitiesFilter.includes(cap)) ); } // Sort models by provider, then by name filteredModels.sort((a, b) => { if (a.provider !== b.provider) { return a.provider.localeCompare(b.provider); } return a.name.localeCompare(b.name); }); const response: ModelsSuccessResponse = { success: true, models: filteredModels, cached: anyFromCache && allFromCache, providers: providerResults, availableProviders, }; if (errors.length > 0) { response.errors = errors; } return NextResponse.json(response); }