Browse Source
Add Next.js API route for server-side fal.ai model fetching: - GET /api/providers/fal/models endpoint - Optional API key auth (fal.ai works without but rate limited) - Filter to image/video categories only - Normalize response to ProviderModel interface - Support search query parameter - Follow established pattern from replicate API route Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 193 additions and 0 deletions
@ -0,0 +1,193 @@ |
|||||
|
import { NextRequest, NextResponse } from "next/server"; |
||||
|
import { ProviderModel, ModelCapability } from "@/lib/providers"; |
||||
|
|
||||
|
const FAL_API_BASE = "https://api.fal.ai/v1"; |
||||
|
|
||||
|
/** |
||||
|
* Categories we care about for image/video generation |
||||
|
*/ |
||||
|
const RELEVANT_CATEGORIES = [ |
||||
|
"text-to-image", |
||||
|
"image-to-image", |
||||
|
"text-to-video", |
||||
|
"image-to-video", |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Response schema from fal.ai models endpoint |
||||
|
*/ |
||||
|
interface FalModelsResponse { |
||||
|
models: FalModel[]; |
||||
|
next_cursor: string | null; |
||||
|
has_more: boolean; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Model schema from fal.ai API |
||||
|
*/ |
||||
|
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<string, unknown>; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Map fal.ai category to ModelCapability |
||||
|
*/ |
||||
|
function mapCategoryToCapability(category: string): ModelCapability | null { |
||||
|
if (RELEVANT_CATEGORIES.includes(category)) { |
||||
|
return category as ModelCapability; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a model has a relevant category |
||||
|
*/ |
||||
|
function isRelevantModel(model: FalModel): boolean { |
||||
|
return RELEVANT_CATEGORIES.includes(model.metadata.category); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Map fal.ai model to our normalized ProviderModel format |
||||
|
*/ |
||||
|
function mapToProviderModel(model: FalModel): ProviderModel { |
||||
|
const capability = mapCategoryToCapability(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, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Build category filter query string for relevant categories |
||||
|
*/ |
||||
|
function buildCategoryFilter(): string { |
||||
|
return RELEVANT_CATEGORIES.map((cat) => `category=${encodeURIComponent(cat)}`).join("&"); |
||||
|
} |
||||
|
|
||||
|
interface ModelsSuccessResponse { |
||||
|
success: true; |
||||
|
models: ProviderModel[]; |
||||
|
} |
||||
|
|
||||
|
interface ModelsErrorResponse { |
||||
|
success: false; |
||||
|
error: string; |
||||
|
} |
||||
|
|
||||
|
type ModelsResponse = ModelsSuccessResponse | ModelsErrorResponse; |
||||
|
|
||||
|
/** |
||||
|
* GET /api/providers/fal/models |
||||
|
* |
||||
|
* Fetches available models from fal.ai API. |
||||
|
* API key is optional - fal.ai works without but with rate limits. |
||||
|
* |
||||
|
* Query params: |
||||
|
* - search: Optional search query to filter models |
||||
|
* - api_key: Alternative to X-API-Key header |
||||
|
*/ |
||||
|
export async function GET( |
||||
|
request: NextRequest |
||||
|
): Promise<NextResponse<ModelsResponse>> { |
||||
|
const requestId = Math.random().toString(36).substring(7); |
||||
|
console.log(`[fal.ai:${requestId}] Models request started`); |
||||
|
|
||||
|
// Get optional API key from header or query param
|
||||
|
const apiKey = |
||||
|
request.headers.get("X-API-Key") || |
||||
|
request.nextUrl.searchParams.get("api_key"); |
||||
|
|
||||
|
const searchQuery = request.nextUrl.searchParams.get("search"); |
||||
|
console.log( |
||||
|
`[fal.ai:${requestId}] ${searchQuery ? `Searching: "${searchQuery}"` : "Listing all models"}${apiKey ? " (with API key)" : " (no API key)"}` |
||||
|
); |
||||
|
|
||||
|
try { |
||||
|
// Build URL with category filter and optional search
|
||||
|
let url = `${FAL_API_BASE}/models?${buildCategoryFilter()}&status=active`; |
||||
|
|
||||
|
if (searchQuery) { |
||||
|
url += `&q=${encodeURIComponent(searchQuery)}`; |
||||
|
} |
||||
|
|
||||
|
// Build headers with optional auth
|
||||
|
const headers: HeadersInit = {}; |
||||
|
if (apiKey) { |
||||
|
headers["Authorization"] = `Key ${apiKey}`; |
||||
|
} |
||||
|
|
||||
|
const response = await fetch(url, { headers }); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
const errorText = await response.text(); |
||||
|
console.error( |
||||
|
`[fal.ai:${requestId}] API error ${response.status}: ${errorText}` |
||||
|
); |
||||
|
|
||||
|
if (response.status === 401) { |
||||
|
return NextResponse.json<ModelsErrorResponse>( |
||||
|
{ |
||||
|
success: false, |
||||
|
error: "Invalid API key", |
||||
|
}, |
||||
|
{ status: 401 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return NextResponse.json<ModelsErrorResponse>( |
||||
|
{ |
||||
|
success: false, |
||||
|
error: `fal.ai API error: ${response.status}`, |
||||
|
}, |
||||
|
{ status: response.status } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const data: FalModelsResponse = await response.json(); |
||||
|
|
||||
|
// Filter to relevant categories and map to ProviderModel
|
||||
|
const models = data.models.filter(isRelevantModel).map(mapToProviderModel); |
||||
|
|
||||
|
console.log(`[fal.ai:${requestId}] Returning ${models.length} models`); |
||||
|
|
||||
|
return NextResponse.json<ModelsSuccessResponse>({ |
||||
|
success: true, |
||||
|
models, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.error(`[fal.ai:${requestId}] Fetch error:`, error); |
||||
|
return NextResponse.json<ModelsErrorResponse>( |
||||
|
{ |
||||
|
success: false, |
||||
|
error: |
||||
|
error instanceof Error |
||||
|
? error.message |
||||
|
: "Failed to fetch models from fal.ai", |
||||
|
}, |
||||
|
{ status: 500 } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue