Browse Source
- GET /api/providers/replicate/models endpoint
- Accepts X-API-Key header or api_key query param
- Optional search param for model search
- Maps Replicate models to ProviderModel interface
- Infers capabilities from model name/description
- Returns { success, models } or { success, error }
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
1 changed files with 214 additions and 0 deletions
@ -0,0 +1,214 @@ |
|||
import { NextRequest, NextResponse } from "next/server"; |
|||
import { ProviderModel, ModelCapability } from "@/lib/providers"; |
|||
|
|||
const REPLICATE_API_BASE = "https://api.replicate.com/v1"; |
|||
|
|||
/** |
|||
* Response schema from Replicate's list models endpoint |
|||
*/ |
|||
interface ReplicateModelsResponse { |
|||
next: string | null; |
|||
previous: string | null; |
|||
results: ReplicateModel[]; |
|||
} |
|||
|
|||
/** |
|||
* Response schema from Replicate's search endpoint |
|||
*/ |
|||
interface ReplicateSearchResponse { |
|||
next: string | null; |
|||
results: ReplicateSearchResult[]; |
|||
} |
|||
|
|||
interface ReplicateSearchResult { |
|||
model: ReplicateModel; |
|||
} |
|||
|
|||
/** |
|||
* Model schema from Replicate API |
|||
*/ |
|||
interface ReplicateModel { |
|||
url: string; |
|||
owner: string; |
|||
name: string; |
|||
description: string | null; |
|||
visibility: "public" | "private"; |
|||
github_url?: string; |
|||
paper_url?: string; |
|||
license_url?: string; |
|||
run_count: number; |
|||
cover_image_url?: string; |
|||
default_example?: Record<string, unknown>; |
|||
latest_version?: { |
|||
id: string; |
|||
openapi_schema?: Record<string, unknown>; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Infer model capabilities from name and description |
|||
*/ |
|||
function inferCapabilities(model: ReplicateModel): ModelCapability[] { |
|||
const capabilities: ModelCapability[] = ["text-to-image"]; |
|||
|
|||
const searchText = `${model.name} ${model.description ?? ""}`.toLowerCase(); |
|||
|
|||
if ( |
|||
searchText.includes("img2img") || |
|||
searchText.includes("image-to-image") || |
|||
searchText.includes("inpaint") || |
|||
searchText.includes("controlnet") |
|||
) { |
|||
capabilities.push("image-to-image"); |
|||
} |
|||
|
|||
if ( |
|||
searchText.includes("video") || |
|||
searchText.includes("animate") || |
|||
searchText.includes("motion") |
|||
) { |
|||
if (searchText.includes("img2vid") || searchText.includes("image-to-video")) { |
|||
capabilities.push("image-to-video"); |
|||
} else { |
|||
capabilities.push("text-to-video"); |
|||
} |
|||
} |
|||
|
|||
return capabilities; |
|||
} |
|||
|
|||
/** |
|||
* Map Replicate model to our normalized ProviderModel format |
|||
*/ |
|||
function mapToProviderModel(model: ReplicateModel): ProviderModel { |
|||
return { |
|||
id: `${model.owner}/${model.name}`, |
|||
name: model.name, |
|||
description: model.description, |
|||
provider: "replicate", |
|||
capabilities: inferCapabilities(model), |
|||
coverImage: model.cover_image_url, |
|||
}; |
|||
} |
|||
|
|||
interface ModelsSuccessResponse { |
|||
success: true; |
|||
models: ProviderModel[]; |
|||
} |
|||
|
|||
interface ModelsErrorResponse { |
|||
success: false; |
|||
error: string; |
|||
} |
|||
|
|||
type ModelsResponse = ModelsSuccessResponse | ModelsErrorResponse; |
|||
|
|||
/** |
|||
* GET /api/providers/replicate/models |
|||
* |
|||
* Fetches available models from Replicate API. |
|||
* Requires API key in X-API-Key header or api_key query param. |
|||
* |
|||
* Query params: |
|||
* - search: Optional search query to filter models |
|||
* - api_key: Alternative to X-API-Key header |
|||
*/ |
|||
export async function GET( |
|||
request: NextRequest |
|||
): Promise<NextResponse<ModelsResponse>> { |
|||
const requestId = Math.random().toString(36).substring(7); |
|||
console.log(`[Replicate:${requestId}] Models request started`); |
|||
|
|||
// Get API key from header or query param
|
|||
const apiKey = |
|||
request.headers.get("X-API-Key") || |
|||
request.nextUrl.searchParams.get("api_key"); |
|||
|
|||
if (!apiKey) { |
|||
console.log(`[Replicate:${requestId}] Missing API key`); |
|||
return NextResponse.json<ModelsErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: "API key required. Provide X-API-Key header or api_key query param.", |
|||
}, |
|||
{ status: 401 } |
|||
); |
|||
} |
|||
|
|||
const searchQuery = request.nextUrl.searchParams.get("search"); |
|||
console.log( |
|||
`[Replicate:${requestId}] ${searchQuery ? `Searching: "${searchQuery}"` : "Listing all models"}` |
|||
); |
|||
|
|||
try { |
|||
let url: string; |
|||
let isSearchRequest = false; |
|||
|
|||
if (searchQuery) { |
|||
url = `${REPLICATE_API_BASE}/search?query=${encodeURIComponent(searchQuery)}`; |
|||
isSearchRequest = true; |
|||
} else { |
|||
url = `${REPLICATE_API_BASE}/models`; |
|||
} |
|||
|
|||
const response = await fetch(url, { |
|||
headers: { |
|||
Authorization: `Bearer ${apiKey}`, |
|||
}, |
|||
}); |
|||
|
|||
if (!response.ok) { |
|||
const errorText = await response.text(); |
|||
console.error( |
|||
`[Replicate:${requestId}] API error ${response.status}: ${errorText}` |
|||
); |
|||
|
|||
if (response.status === 401) { |
|||
return NextResponse.json<ModelsErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: "Invalid API key", |
|||
}, |
|||
{ status: 401 } |
|||
); |
|||
} |
|||
|
|||
return NextResponse.json<ModelsErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: `Replicate API error: ${response.status}`, |
|||
}, |
|||
{ status: response.status } |
|||
); |
|||
} |
|||
|
|||
let models: ProviderModel[]; |
|||
|
|||
if (isSearchRequest) { |
|||
const data: ReplicateSearchResponse = await response.json(); |
|||
models = data.results.map((result) => mapToProviderModel(result.model)); |
|||
} else { |
|||
const data: ReplicateModelsResponse = await response.json(); |
|||
models = data.results.map(mapToProviderModel); |
|||
} |
|||
|
|||
console.log(`[Replicate:${requestId}] Returning ${models.length} models`); |
|||
|
|||
return NextResponse.json<ModelsSuccessResponse>({ |
|||
success: true, |
|||
models, |
|||
}); |
|||
} catch (error) { |
|||
console.error(`[Replicate:${requestId}] Fetch error:`, error); |
|||
return NextResponse.json<ModelsErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: |
|||
error instanceof Error |
|||
? error.message |
|||
: "Failed to fetch models from Replicate", |
|||
}, |
|||
{ status: 500 } |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue