From d9c0ce75cb8a124e39812ffe84a6acf24bd40e16 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 9 Jan 2026 20:31:16 +1300 Subject: [PATCH] feat(03-02): implement fal.ai provider generate() method Implement generation via fal.ai's fal.run API: - POST to https://fal.run/{modelId} with prompt and parameters - Use "Key {apiKey}" auth header (works without key but rate limited) - Extract image URL from response (handles images array, image object, or output string) - Fetch image and convert to base64 data URL - Return GenerationOutput with type: "image" Note: Image inputs skipped for now (Phase 5 adds URL server) Co-Authored-By: Claude Opus 4.5 --- src/lib/providers/fal.ts | 107 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/src/lib/providers/fal.ts b/src/lib/providers/fal.ts index 03300276..6e4c7495 100644 --- a/src/lib/providers/fal.ts +++ b/src/lib/providers/fal.ts @@ -72,6 +72,16 @@ interface FalModel { openapi?: Record; } +/** + * Generation response from fal.ai + * Different models may return different structures + */ +interface FalGenerationResponse { + images?: Array<{ url: string; content_type?: string }>; + image?: { url: string; content_type?: string }; + output?: string; +} + /** * Get API key from localStorage (client-side only) * Returns null when running on server or if not configured @@ -232,12 +242,97 @@ const falProvider: ProviderInterface = { } }, - async generate(_input: GenerationInput): Promise { - // Generation will be implemented in Phase 3 - return { - success: false, - error: "Not implemented - generation support coming in Phase 3", - }; + async generate(input: GenerationInput): Promise { + const apiKey = getApiKeyFromStorage(); + // Note: fal.ai works without API key but with rate limits + + try { + const modelId = input.model.id; + + // Build request body + // fal.ai models typically expect "prompt" as input + const requestBody: Record = { + prompt: input.prompt, + ...input.parameters, + }; + + // Note: Image inputs are skipped for now (Phase 5 adds URL server) + // input.images would need to be converted to URLs for fal.ai + + // POST to fal.run/{modelId} + const response = await fetch(`https://fal.run/${modelId}`, { + method: "POST", + headers: { + ...buildHeaders(apiKey), + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + return { + success: false, + error: `fal.ai API error: ${response.status} - ${errorText}`, + }; + } + + const result: FalGenerationResponse = await response.json(); + + // fal.ai response typically has "images" array with url field + // or "image" object with url field depending on the model + let imageUrl: string | null = null; + + if (result.images && Array.isArray(result.images) && result.images.length > 0) { + imageUrl = result.images[0].url; + } else if (result.image && result.image.url) { + imageUrl = result.image.url; + } else if (result.output && typeof result.output === "string") { + // Some models return URL directly in output + imageUrl = result.output; + } + + if (!imageUrl) { + return { + success: false, + error: "No image URL in response", + }; + } + + // Fetch the image and convert to base64 + const imageResponse = await fetch(imageUrl); + + if (!imageResponse.ok) { + return { + success: false, + error: `Failed to fetch output image: ${imageResponse.status}`, + }; + } + + const imageArrayBuffer = await imageResponse.arrayBuffer(); + const imageBase64 = Buffer.from(imageArrayBuffer).toString("base64"); + + // Determine MIME type from response + const contentType = + imageResponse.headers.get("content-type") || "image/png"; + + return { + success: true, + outputs: [ + { + type: "image", + data: `data:${contentType};base64,${imageBase64}`, + url: imageUrl, + }, + ], + }; + } catch (error) { + console.error("[fal.ai] Generation failed:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Generation failed", + }; + } }, isConfigured(): boolean {