Browse Source
- Create /api/models/[modelId] endpoint for fetching model parameter schemas - Add ModelParameter interface to lib/providers/types.ts - Support both Replicate and fal.ai providers - Filter out internal params, prioritize user-relevant ones - 10-minute cache TTL for schema responseshandoff-20260429-1057
2 changed files with 343 additions and 0 deletions
@ -0,0 +1,329 @@ |
|||
/** |
|||
* Model Schema API Endpoint |
|||
* |
|||
* Fetches parameter schema for a specific model from its provider. |
|||
* Returns simplified parameter list for UI rendering. |
|||
* |
|||
* GET /api/models/:modelId?provider=replicate|fal |
|||
* |
|||
* Headers: |
|||
* - X-Replicate-Key: Required for Replicate models |
|||
* - X-Fal-Key: Optional for fal.ai models |
|||
* |
|||
* Response: |
|||
* { |
|||
* success: true, |
|||
* parameters: ModelParameter[], |
|||
* cached: boolean |
|||
* } |
|||
*/ |
|||
|
|||
import { NextRequest, NextResponse } from "next/server"; |
|||
import { ProviderType } from "@/types"; |
|||
import { ModelParameter } from "@/lib/providers/types"; |
|||
|
|||
// Cache for model schemas (10 minute TTL)
|
|||
const schemaCache = new Map<string, { parameters: ModelParameter[]; timestamp: number }>(); |
|||
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
|
|||
|
|||
// Parameters to filter out (internal/system params)
|
|||
const EXCLUDED_PARAMS = new Set([ |
|||
"webhook", |
|||
"webhook_events_filter", |
|||
"sync_mode", |
|||
"disable_safety_checker", |
|||
"go_fast", |
|||
"enable_safety_checker", |
|||
"output_format", |
|||
"output_quality", |
|||
"request_id", |
|||
]); |
|||
|
|||
// Parameters we want to surface (user-relevant)
|
|||
const PRIORITY_PARAMS = new Set([ |
|||
"seed", |
|||
"num_inference_steps", |
|||
"inference_steps", |
|||
"steps", |
|||
"guidance_scale", |
|||
"guidance", |
|||
"negative_prompt", |
|||
"width", |
|||
"height", |
|||
"num_outputs", |
|||
"num_images", |
|||
"scheduler", |
|||
"strength", |
|||
"cfg_scale", |
|||
"lora_scale", |
|||
]); |
|||
|
|||
interface SchemaSuccessResponse { |
|||
success: true; |
|||
parameters: ModelParameter[]; |
|||
cached: boolean; |
|||
} |
|||
|
|||
interface SchemaErrorResponse { |
|||
success: false; |
|||
error: string; |
|||
} |
|||
|
|||
type SchemaResponse = SchemaSuccessResponse | SchemaErrorResponse; |
|||
|
|||
/** |
|||
* Convert OpenAPI schema property to ModelParameter |
|||
*/ |
|||
function convertSchemaProperty( |
|||
name: string, |
|||
prop: Record<string, unknown>, |
|||
required: string[] |
|||
): ModelParameter | null { |
|||
// Skip excluded parameters
|
|||
if (EXCLUDED_PARAMS.has(name)) { |
|||
return null; |
|||
} |
|||
|
|||
// Determine type
|
|||
let type: ModelParameter["type"] = "string"; |
|||
const schemaType = prop.type as string | undefined; |
|||
const allOf = prop.allOf as Array<Record<string, unknown>> | undefined; |
|||
|
|||
if (schemaType === "integer") { |
|||
type = "integer"; |
|||
} else if (schemaType === "number") { |
|||
type = "number"; |
|||
} else if (schemaType === "boolean") { |
|||
type = "boolean"; |
|||
} else if (schemaType === "array") { |
|||
type = "array"; |
|||
} else if (allOf && allOf.length > 0) { |
|||
// Handle allOf with enum reference
|
|||
type = "string"; |
|||
} |
|||
|
|||
const parameter: ModelParameter = { |
|||
name, |
|||
type, |
|||
description: prop.description as string | undefined, |
|||
default: prop.default, |
|||
required: required.includes(name), |
|||
}; |
|||
|
|||
// Add constraints
|
|||
if (typeof prop.minimum === "number") { |
|||
parameter.minimum = prop.minimum; |
|||
} |
|||
if (typeof prop.maximum === "number") { |
|||
parameter.maximum = prop.maximum; |
|||
} |
|||
if (Array.isArray(prop.enum)) { |
|||
parameter.enum = prop.enum; |
|||
} |
|||
|
|||
return parameter; |
|||
} |
|||
|
|||
/** |
|||
* Fetch and parse schema from Replicate |
|||
*/ |
|||
async function fetchReplicateSchema( |
|||
modelId: string, |
|||
apiKey: string |
|||
): Promise<ModelParameter[]> { |
|||
const [owner, name] = modelId.split("/"); |
|||
|
|||
const response = await fetch( |
|||
`https://api.replicate.com/v1/models/${owner}/${name}`, |
|||
{ |
|||
headers: { |
|||
Authorization: `Bearer ${apiKey}`, |
|||
}, |
|||
} |
|||
); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`Replicate API error: ${response.status}`); |
|||
} |
|||
|
|||
const data = await response.json(); |
|||
|
|||
// Extract schema from latest_version.openapi_schema
|
|||
const openApiSchema = data.latest_version?.openapi_schema; |
|||
if (!openApiSchema) { |
|||
return []; |
|||
} |
|||
|
|||
// Navigate to Input schema
|
|||
const inputSchema = openApiSchema.components?.schemas?.Input; |
|||
if (!inputSchema || typeof inputSchema !== "object") { |
|||
return []; |
|||
} |
|||
|
|||
const properties = (inputSchema as Record<string, unknown>).properties as Record<string, Record<string, unknown>> | undefined; |
|||
const required = ((inputSchema as Record<string, unknown>).required as string[]) || []; |
|||
|
|||
if (!properties) { |
|||
return []; |
|||
} |
|||
|
|||
const parameters: ModelParameter[] = []; |
|||
|
|||
for (const [name, prop] of Object.entries(properties)) { |
|||
// Skip prompt as it's handled separately
|
|||
if (name === "prompt") continue; |
|||
|
|||
const param = convertSchemaProperty(name, prop, required); |
|||
if (param) { |
|||
parameters.push(param); |
|||
} |
|||
} |
|||
|
|||
// Sort: priority params first, then alphabetically
|
|||
parameters.sort((a, b) => { |
|||
const aIsPriority = PRIORITY_PARAMS.has(a.name); |
|||
const bIsPriority = PRIORITY_PARAMS.has(b.name); |
|||
if (aIsPriority && !bIsPriority) return -1; |
|||
if (!aIsPriority && bIsPriority) return 1; |
|||
return a.name.localeCompare(b.name); |
|||
}); |
|||
|
|||
return parameters; |
|||
} |
|||
|
|||
/** |
|||
* Fetch and parse schema from fal.ai |
|||
*/ |
|||
async function fetchFalSchema( |
|||
modelId: string, |
|||
apiKey: string | null |
|||
): Promise<ModelParameter[]> { |
|||
const headers: Record<string, string> = {}; |
|||
if (apiKey) { |
|||
headers["Authorization"] = `Key ${apiKey}`; |
|||
} |
|||
|
|||
// Fetch OpenAPI spec from /api endpoint
|
|||
const response = await fetch(`https://fal.run/${modelId}/api`, { headers }); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`fal.ai API error: ${response.status}`); |
|||
} |
|||
|
|||
const spec = await response.json(); |
|||
|
|||
// Navigate to request body schema
|
|||
// Path: paths["/"]["post"].requestBody.content["application/json"].schema
|
|||
const requestBodySchema = spec.paths?.["/"]?.post?.requestBody?.content?.["application/json"]?.schema; |
|||
|
|||
if (!requestBodySchema) { |
|||
return []; |
|||
} |
|||
|
|||
const properties = requestBodySchema.properties as Record<string, Record<string, unknown>> | undefined; |
|||
const required = (requestBodySchema.required as string[]) || []; |
|||
|
|||
if (!properties) { |
|||
return []; |
|||
} |
|||
|
|||
const parameters: ModelParameter[] = []; |
|||
|
|||
for (const [name, prop] of Object.entries(properties)) { |
|||
// Skip prompt and image_url as they're handled separately
|
|||
if (name === "prompt" || name === "image_url") continue; |
|||
|
|||
const param = convertSchemaProperty(name, prop, required); |
|||
if (param) { |
|||
parameters.push(param); |
|||
} |
|||
} |
|||
|
|||
// Sort: priority params first, then alphabetically
|
|||
parameters.sort((a, b) => { |
|||
const aIsPriority = PRIORITY_PARAMS.has(a.name); |
|||
const bIsPriority = PRIORITY_PARAMS.has(b.name); |
|||
if (aIsPriority && !bIsPriority) return -1; |
|||
if (!aIsPriority && bIsPriority) return 1; |
|||
return a.name.localeCompare(b.name); |
|||
}); |
|||
|
|||
return parameters; |
|||
} |
|||
|
|||
export async function GET( |
|||
request: NextRequest, |
|||
{ params }: { params: Promise<{ modelId: string }> } |
|||
): Promise<NextResponse<SchemaResponse>> { |
|||
const requestId = Math.random().toString(36).substring(7); |
|||
|
|||
// Await params before accessing properties
|
|||
const { modelId } = await params; |
|||
const decodedModelId = decodeURIComponent(modelId); |
|||
const provider = request.nextUrl.searchParams.get("provider") as ProviderType | null; |
|||
|
|||
console.log(`[ModelSchema:${requestId}] Fetching schema for ${decodedModelId} (provider: ${provider})`); |
|||
|
|||
if (!provider || (provider !== "replicate" && provider !== "fal")) { |
|||
return NextResponse.json<SchemaErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: "Invalid or missing provider. Use ?provider=replicate or ?provider=fal", |
|||
}, |
|||
{ status: 400 } |
|||
); |
|||
} |
|||
|
|||
// Check cache
|
|||
const cacheKey = `${provider}:${decodedModelId}`; |
|||
const cached = schemaCache.get(cacheKey); |
|||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) { |
|||
console.log(`[ModelSchema:${requestId}] Cache hit, returning ${cached.parameters.length} parameters`); |
|||
return NextResponse.json<SchemaSuccessResponse>({ |
|||
success: true, |
|||
parameters: cached.parameters, |
|||
cached: true, |
|||
}); |
|||
} |
|||
|
|||
try { |
|||
let parameters: ModelParameter[]; |
|||
|
|||
if (provider === "replicate") { |
|||
const apiKey = request.headers.get("X-Replicate-Key"); |
|||
if (!apiKey) { |
|||
return NextResponse.json<SchemaErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: "Replicate API key required. Include X-Replicate-Key header.", |
|||
}, |
|||
{ status: 401 } |
|||
); |
|||
} |
|||
parameters = await fetchReplicateSchema(decodedModelId, apiKey); |
|||
} else { |
|||
const apiKey = request.headers.get("X-Fal-Key"); |
|||
parameters = await fetchFalSchema(decodedModelId, apiKey); |
|||
} |
|||
|
|||
// Cache the result
|
|||
schemaCache.set(cacheKey, { parameters, timestamp: Date.now() }); |
|||
|
|||
console.log(`[ModelSchema:${requestId}] Returning ${parameters.length} parameters`); |
|||
return NextResponse.json<SchemaSuccessResponse>({ |
|||
success: true, |
|||
parameters, |
|||
cached: false, |
|||
}); |
|||
} catch (error) { |
|||
const errorMessage = error instanceof Error ? error.message : "Unknown error"; |
|||
console.error(`[ModelSchema:${requestId}] Error: ${errorMessage}`); |
|||
return NextResponse.json<SchemaErrorResponse>( |
|||
{ |
|||
success: false, |
|||
error: errorMessage, |
|||
}, |
|||
{ status: 500 } |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue