diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 9550f9e7..47e100c4 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -307,9 +307,25 @@ async function generateWithReplicate( if (!createResponse.ok) { const errorText = await createResponse.text(); + let errorDetail = errorText; + try { + const errorJson = JSON.parse(errorText); + errorDetail = errorJson.detail || errorJson.message || errorJson.error || errorText; + } catch { + // Keep original text if not JSON + } + + // Handle rate limits + if (createResponse.status === 429) { + return { + success: false, + error: `${input.model.name}: Rate limit exceeded. Try again in a moment.`, + }; + } + return { success: false, - error: `Failed to create prediction: ${createResponse.status} - ${errorText}`, + error: `${input.model.name}: ${errorDetail}`, }; } @@ -330,7 +346,7 @@ async function generateWithReplicate( if (Date.now() - startTime > maxWaitTime) { return { success: false, - error: "Prediction timed out after 5 minutes", + error: `${input.model.name}: Generation timed out after 5 minutes. Video models may take longer - try again.`, }; } @@ -357,9 +373,11 @@ async function generateWithReplicate( } if (currentPrediction.status === "failed") { + const failureReason = currentPrediction.error || "Prediction failed"; + console.error(`[API:${requestId}] Replicate prediction failed: ${failureReason}`); return { success: false, - error: currentPrediction.error || "Prediction failed", + error: `${input.model.name}: ${failureReason}`, }; } @@ -496,9 +514,25 @@ async function generateWithFal( if (!response.ok) { const errorText = await response.text(); + let errorDetail = errorText; + try { + const errorJson = JSON.parse(errorText); + errorDetail = errorJson.detail || errorJson.message || errorJson.error || errorText; + } catch { + // Keep original text if not JSON + } + + // Handle rate limits + if (response.status === 429) { + return { + success: false, + error: `${input.model.name}: Rate limit exceeded. ${apiKey ? "Try again in a moment." : "Add an API key in settings for higher limits."}`, + }; + } + return { success: false, - error: `fal.ai API error: ${response.status} - ${errorText}`, + error: `${input.model.name}: ${errorDetail}`, }; } diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 775bfff0..75021c10 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -33,6 +33,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps([]); const [isLoadingModels, setIsLoadingModels] = useState(false); + const [modelsFetchError, setModelsFetchError] = useState(null); // Get the current selected provider (default to gemini) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini"; @@ -66,40 +67,51 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { - const fetchModels = async () => { - if (currentProvider === "gemini") { - setExternalModels([]); - return; - } + const fetchModels = useCallback(async () => { + if (currentProvider === "gemini") { + setExternalModels([]); + setModelsFetchError(null); + return; + } - setIsLoadingModels(true); - try { - const capabilities = IMAGE_CAPABILITIES.join(","); - const headers: HeadersInit = {}; - if (providerSettings.providers.replicate?.apiKey) { - headers["X-Replicate-Key"] = providerSettings.providers.replicate.apiKey; - } - if (providerSettings.providers.fal?.apiKey) { - headers["X-Fal-Key"] = providerSettings.providers.fal.apiKey; - } - const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); - if (response.ok) { - const data = await response.json(); - setExternalModels(data.models || []); - } else { - setExternalModels([]); - } - } catch (error) { - console.error("Failed to fetch models:", error); + setIsLoadingModels(true); + setModelsFetchError(null); + try { + const capabilities = IMAGE_CAPABILITIES.join(","); + const headers: HeadersInit = {}; + if (providerSettings.providers.replicate?.apiKey) { + headers["X-Replicate-Key"] = providerSettings.providers.replicate.apiKey; + } + if (providerSettings.providers.fal?.apiKey) { + headers["X-Fal-Key"] = providerSettings.providers.fal.apiKey; + } + const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); + if (response.ok) { + const data = await response.json(); + setExternalModels(data.models || []); + setModelsFetchError(null); + } else { + const errorData = await response.json().catch(() => ({})); + const errorMsg = errorData.error || `Failed to load models (${response.status})`; setExternalModels([]); - } finally { - setIsLoadingModels(false); + setModelsFetchError( + currentProvider === "replicate" && response.status === 401 + ? "Invalid Replicate API key. Check your settings." + : errorMsg + ); } - }; + } catch (error) { + console.error("Failed to fetch models:", error); + setExternalModels([]); + setModelsFetchError("Failed to load models. Check your connection."); + } finally { + setIsLoadingModels(false); + } + }, [currentProvider, providerSettings]); + useEffect(() => { fetchModels(); - }, [currentProvider, providerSettings]); + }, [fetchModels]); // Handle provider change const handleProviderChange = useCallback( @@ -481,30 +493,43 @@ export function GenerateImageNode({ id, data, selected }: NodeProps ) : ( <> - + {/* Error state with retry button */} + {modelsFetchError ? ( +
+ {modelsFetchError} + +
+ ) : ( + + )} {/* Model-specific parameters for external providers */} - {nodeData.selectedModel?.modelId && ( + {nodeData.selectedModel?.modelId && !modelsFetchError && ( state.providerSettings); const [externalModels, setExternalModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); + const [modelsFetchError, setModelsFetchError] = useState(null); // Get the current selected provider (default to fal since Gemini doesn't do video) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; @@ -36,35 +37,45 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { - const fetchModels = async () => { - setIsLoadingModels(true); - try { - const capabilities = VIDEO_CAPABILITIES.join(","); - const headers: HeadersInit = {}; - if (providerSettings.providers.replicate?.apiKey) { - headers["X-Replicate-Key"] = providerSettings.providers.replicate.apiKey; - } - if (providerSettings.providers.fal?.apiKey) { - headers["X-Fal-Key"] = providerSettings.providers.fal.apiKey; - } - const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); - if (response.ok) { - const data = await response.json(); - setExternalModels(data.models || []); - } else { - setExternalModels([]); - } - } catch (error) { - console.error("Failed to fetch video models:", error); + const fetchModels = useCallback(async () => { + setIsLoadingModels(true); + setModelsFetchError(null); + try { + const capabilities = VIDEO_CAPABILITIES.join(","); + const headers: HeadersInit = {}; + if (providerSettings.providers.replicate?.apiKey) { + headers["X-Replicate-Key"] = providerSettings.providers.replicate.apiKey; + } + if (providerSettings.providers.fal?.apiKey) { + headers["X-Fal-Key"] = providerSettings.providers.fal.apiKey; + } + const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); + if (response.ok) { + const data = await response.json(); + setExternalModels(data.models || []); + setModelsFetchError(null); + } else { + const errorData = await response.json().catch(() => ({})); + const errorMsg = errorData.error || `Failed to load models (${response.status})`; setExternalModels([]); - } finally { - setIsLoadingModels(false); + setModelsFetchError( + currentProvider === "replicate" && response.status === 401 + ? "Invalid Replicate API key. Check your settings." + : errorMsg + ); } - }; + } catch (error) { + console.error("Failed to fetch video models:", error); + setExternalModels([]); + setModelsFetchError("Failed to load models. Check your connection."); + } finally { + setIsLoadingModels(false); + } + }, [currentProvider, providerSettings]); + useEffect(() => { fetchModels(); - }, [currentProvider, providerSettings]); + }, [fetchModels]); // Handle provider change const handleProviderChange = useCallback( @@ -250,30 +261,42 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps - {isLoadingModels ? ( - - ) : externalModels.length === 0 ? ( - - ) : ( - <> - - {externalModels.map((m) => ( - - ))} - - )} - + {modelsFetchError ? ( +
+ {modelsFetchError} + +
+ ) : ( + + )} {/* Model-specific parameters */} - {nodeData.selectedModel?.modelId && ( + {nodeData.selectedModel?.modelId && !modelsFetchError && (