Browse Source

feat(02-01): create Replicate models API route

- 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
shrimbly 6 months ago
parent
commit
70d4c53308
  1. 214
      src/app/api/providers/replicate/models/route.ts

214
src/app/api/providers/replicate/models/route.ts

@ -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…
Cancel
Save