From 69217d9a8010f1c25f85c453bed556687b2519d6 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 9 Jan 2026 20:20:43 +1300 Subject: [PATCH] fix(03-01): add pagination to Replicate and fal.ai model fetching Both APIs return paginated results but we were only fetching page 1. Now fetches up to 5 pages from Replicate (~125 models) and 10 pages from fal.ai to get more complete model lists. --- src/app/api/models/route.ts | 87 +++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index 7dab2c10..ea77baf6 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -184,7 +184,8 @@ async function fetchReplicateModels( apiKey: string, searchQuery?: string ): Promise { - let url: string; + const allModels: ProviderModel[] = []; + let url: string | null; let isSearchRequest = false; if (searchQuery) { @@ -194,25 +195,38 @@ async function fetchReplicateModels( url = `${REPLICATE_API_BASE}/models`; } - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); + // Paginate through all results (limit to 5 pages = ~125 models to avoid timeout) + let pageCount = 0; + const maxPages = 5; - if (!response.ok) { - throw new Error(`Replicate API error: ${response.status}`); - } + while (url && pageCount < maxPages) { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); - if (isSearchRequest) { - const data: ReplicateSearchResponse = await response.json(); - if (!data.results) return []; - return data.results.map((result) => mapReplicateModel(result.model)); - } else { - const data: ReplicateModelsResponse = await response.json(); - if (!data.results) return []; - return data.results.map(mapReplicateModel); + if (!response.ok) { + throw new Error(`Replicate API error: ${response.status}`); + } + + if (isSearchRequest) { + const data: ReplicateSearchResponse = await response.json(); + if (data.results) { + allModels.push(...data.results.map((result) => mapReplicateModel(result.model))); + } + url = data.next; + } else { + const data: ReplicateModelsResponse = await response.json(); + if (data.results) { + allModels.push(...data.results.map(mapReplicateModel)); + } + url = data.next; + } + pageCount++; } + + return allModels; } // ============ Fal.ai Helpers ============ @@ -245,26 +259,43 @@ async function fetchFalModels( apiKey: string | null, searchQuery?: string ): Promise { - let url = `${FAL_API_BASE}/models?status=active`; - - if (searchQuery) { - url += `&q=${encodeURIComponent(searchQuery)}`; - } + const allModels: ProviderModel[] = []; + let cursor: string | null = null; + let hasMore = true; const headers: HeadersInit = {}; if (apiKey) { headers["Authorization"] = `Key ${apiKey}`; } - const response = await fetch(url, { headers }); + // Paginate through all results (limit to 10 pages to avoid timeout) + let pageCount = 0; + const maxPages = 10; - if (!response.ok) { - throw new Error(`fal.ai API error: ${response.status}`); - } + while (hasMore && pageCount < maxPages) { + let url = `${FAL_API_BASE}/models?status=active`; + if (searchQuery) { + url += `&q=${encodeURIComponent(searchQuery)}`; + } + if (cursor) { + url += `&cursor=${encodeURIComponent(cursor)}`; + } + + const response = await fetch(url, { headers }); + + if (!response.ok) { + throw new Error(`fal.ai API error: ${response.status}`); + } + + const data: FalModelsResponse = await response.json(); + allModels.push(...data.models.filter(isRelevantFalModel).map(mapFalModel)); - const data: FalModelsResponse = await response.json(); + cursor = data.next_cursor; + hasMore = data.has_more; + pageCount++; + } - return data.models.filter(isRelevantFalModel).map(mapFalModel); + return allModels; } // ============ Main Handler ============