You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

257 lines
6.6 KiB

/**
* fal.ai Provider Implementation
*
* Implements ProviderInterface for fal.ai's AI model marketplace.
* Provides model discovery via fal.ai's REST API and self-registers
* in the provider registry when imported.
*
* Usage:
* import "@/lib/providers/fal"; // Just importing registers the provider
*
* // Or get it from registry:
* import { getProvider } from "@/lib/providers";
* const fal = getProvider("fal");
*
* Note: fal.ai works without an API key but with rate limits.
* Providing a key enables higher rate limits.
*/
import {
ProviderInterface,
ProviderModel,
ModelCapability,
GenerationInput,
GenerationOutput,
registerProvider,
} from "@/lib/providers";
const FAL_API_BASE = "https://api.fal.ai/v1";
const PROVIDER_SETTINGS_KEY = "node-banana-provider-settings";
/**
* 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>;
}
/**
* Get API key from localStorage (client-side only)
* Returns null when running on server or if not configured
*/
function getApiKeyFromStorage(): string | null {
if (typeof window === "undefined") {
return null;
}
try {
const settingsJson = localStorage.getItem(PROVIDER_SETTINGS_KEY);
if (!settingsJson) return null;
const settings = JSON.parse(settingsJson);
return settings?.providers?.fal?.apiKey ?? null;
} catch {
return null;
}
}
/**
* Map fal.ai category to ModelCapability
* Returns the capability if it's one we support, null otherwise
*/
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 authorization headers for fal.ai API
*/
function buildHeaders(apiKey: string | null): HeadersInit {
const headers: HeadersInit = {};
if (apiKey) {
headers["Authorization"] = `Key ${apiKey}`;
}
return headers;
}
/**
* fal.ai provider implementation
*/
const falProvider: ProviderInterface = {
id: "fal",
name: "fal.ai",
async listModels(): Promise<ProviderModel[]> {
const apiKey = getApiKeyFromStorage();
try {
// Fetch all active models, filter client-side
// Note: fal.ai API only accepts single category param, so we fetch all and filter
const url = `${FAL_API_BASE}/models?status=active`;
const response = await fetch(url, {
headers: buildHeaders(apiKey),
});
if (!response.ok) {
throw new Error(`fal.ai API error: ${response.status}`);
}
const data: FalModelsResponse = await response.json();
// Filter to relevant categories and map to ProviderModel
return data.models.filter(isRelevantModel).map(mapToProviderModel);
} catch (error) {
console.error("[fal.ai] Failed to list models:", error);
return [];
}
},
async searchModels(query: string): Promise<ProviderModel[]> {
const apiKey = getApiKeyFromStorage();
try {
// Search with query, filter client-side
// Note: fal.ai API only accepts single category param, so we fetch all and filter
const url = `${FAL_API_BASE}/models?q=${encodeURIComponent(query)}&status=active`;
const response = await fetch(url, {
headers: buildHeaders(apiKey),
});
if (!response.ok) {
throw new Error(`fal.ai API error: ${response.status}`);
}
const data: FalModelsResponse = await response.json();
// Filter to relevant categories and map to ProviderModel
return data.models.filter(isRelevantModel).map(mapToProviderModel);
} catch (error) {
console.error("[fal.ai] Failed to search models:", error);
return [];
}
},
async getModel(modelId: string): Promise<ProviderModel | null> {
const apiKey = getApiKeyFromStorage();
try {
// Fetch specific model by endpoint_id
const url = `${FAL_API_BASE}/models?endpoint_id=${encodeURIComponent(modelId)}`;
const response = await fetch(url, {
headers: buildHeaders(apiKey),
});
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`fal.ai API error: ${response.status}`);
}
const data: FalModelsResponse = await response.json();
if (data.models.length === 0) {
return null;
}
const model = data.models[0];
// Check if it's a relevant model type
if (!isRelevantModel(model)) {
console.warn(`[fal.ai] Model ${modelId} is not an image/video model`);
return null;
}
return mapToProviderModel(model);
} catch (error) {
console.error("[fal.ai] Failed to get model:", error);
return null;
}
},
async generate(_input: GenerationInput): Promise<GenerationOutput> {
// Generation will be implemented in Phase 3
return {
success: false,
error: "Not implemented - generation support coming in Phase 3",
};
},
isConfigured(): boolean {
// fal.ai works without API key (with rate limits)
// Return true if key exists, but note that provider is usable without it
return !!getApiKeyFromStorage();
},
getApiKey(): string | null {
return getApiKeyFromStorage();
},
};
// Self-register when module is imported
registerProvider(falProvider);
export default falProvider;