Browse Source

fix(04-02): use client-side search filtering for Replicate

- Replicate's search API endpoint unreliable, use client-side filtering
- Always cache full model list for Replicate (not per-search-query)
- Apply search filter on cached models for faster subsequent searches
- Remove unused ReplicateSearchResponse and ReplicateSearchResult types
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
4221225f52
  1. 100
      src/app/api/models/route.ts

100
src/app/api/models/route.ts

@ -54,15 +54,6 @@ interface ReplicateModelsResponse {
results: ReplicateModel[]; results: ReplicateModel[];
} }
interface ReplicateSearchResponse {
next: string | null;
results: ReplicateSearchResult[];
}
interface ReplicateSearchResult {
model: ReplicateModel;
}
interface ReplicateModel { interface ReplicateModel {
url: string; url: string;
owner: string; owner: string;
@ -192,20 +183,11 @@ function mapReplicateModel(model: ReplicateModel): ProviderModel {
}; };
} }
async function fetchReplicateModels( async function fetchReplicateModels(apiKey: string): Promise<ProviderModel[]> {
apiKey: string,
searchQuery?: string
): Promise<ProviderModel[]> {
const allModels: ProviderModel[] = []; const allModels: ProviderModel[] = [];
let url: string | null;
let isSearchRequest = false;
if (searchQuery) { // Always fetch from the models endpoint - search endpoint is unreliable
url = `${REPLICATE_API_BASE}/search?query=${encodeURIComponent(searchQuery)}`; let url: string | null = `${REPLICATE_API_BASE}/models`;
isSearchRequest = true;
} else {
url = `${REPLICATE_API_BASE}/models`;
}
// Paginate through results (limit to 15 pages to avoid timeout) // Paginate through results (limit to 15 pages to avoid timeout)
let pageCount = 0; let pageCount = 0;
@ -222,25 +204,34 @@ async function fetchReplicateModels(
throw new Error(`Replicate API error: ${response.status}`); throw new Error(`Replicate API error: ${response.status}`);
} }
if (isSearchRequest) { const data: ReplicateModelsResponse = await response.json();
const data: ReplicateSearchResponse = await response.json(); if (data.results) {
if (data.results) { allModels.push(...data.results.map(mapReplicateModel));
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;
} }
url = data.next;
pageCount++; pageCount++;
} }
return allModels; return allModels;
} }
/**
* Filter models by search query (client-side filtering for Replicate)
*/
function filterModelsBySearch(
models: ProviderModel[],
searchQuery: string
): ProviderModel[] {
const searchLower = searchQuery.toLowerCase();
return models.filter((model) => {
const nameMatch = model.name.toLowerCase().includes(searchLower);
const descMatch =
model.description?.toLowerCase().includes(searchLower) || false;
const idMatch = model.id.toLowerCase().includes(searchLower);
return nameMatch || descMatch || idMatch;
});
}
// ============ Fal.ai Helpers ============ // ============ Fal.ai Helpers ============
function mapFalCategory(category: string): ModelCapability | null { function mapFalCategory(category: string): ModelCapability | null {
@ -380,7 +371,12 @@ export async function GET(
// Fetch from each provider // Fetch from each provider
for (const provider of providersToFetch) { for (const provider of providersToFetch) {
const cacheKey = getCacheKey(provider, searchQuery); // For Replicate, always use base cache key since we filter client-side
// For fal.ai, include search in cache key since their API supports search
const cacheKey =
provider === "replicate"
? getCacheKey(provider)
: getCacheKey(provider, searchQuery);
let models: ProviderModel[] | null = null; let models: ProviderModel[] | null = null;
let fromCache = false; let fromCache = false;
@ -394,6 +390,14 @@ export async function GET(
console.log( console.log(
`[Models:${requestId}] ${provider}: Using cached models (${cached.length})` `[Models:${requestId}] ${provider}: Using cached models (${cached.length})`
); );
// For Replicate, apply client-side search filtering on cached models
if (provider === "replicate" && searchQuery) {
models = filterModelsBySearch(models, searchQuery);
console.log(
`[Models:${requestId}] ${provider}: Filtered to ${models.length} models for search "${searchQuery}"`
);
}
} }
} }
@ -402,18 +406,32 @@ export async function GET(
allFromCache = false; allFromCache = false;
try { try {
if (provider === "replicate") { if (provider === "replicate") {
models = await fetchReplicateModels(replicateKey!, searchQuery); // Fetch all models (no search param - we filter client-side)
const allReplicateModels = await fetchReplicateModels(replicateKey!);
// Cache the full list
setCachedModels(cacheKey, allReplicateModels);
console.log(
`[Models:${requestId}] ${provider}: Fetched ${allReplicateModels.length} models from API`
);
// Apply search filter if needed
models = searchQuery
? filterModelsBySearch(allReplicateModels, searchQuery)
: allReplicateModels;
if (searchQuery) {
console.log(
`[Models:${requestId}] ${provider}: Filtered to ${models.length} models for search "${searchQuery}"`
);
}
} else if (provider === "fal") { } else if (provider === "fal") {
models = await fetchFalModels(falKey, searchQuery); models = await fetchFalModels(falKey, searchQuery);
// Cache the results (fal.ai handles search server-side)
setCachedModels(cacheKey, models);
console.log(
`[Models:${requestId}] ${provider}: Fetched ${models.length} models from API`
);
} else { } else {
models = []; models = [];
} }
// Store in cache
setCachedModels(cacheKey, models);
console.log(
`[Models:${requestId}] ${provider}: Fetched ${models.length} models from API`
);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "Unknown error"; error instanceof Error ? error.message : "Unknown error";

Loading…
Cancel
Save