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
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;
|
|
|