import { z } from "zod"; import { requireAdmin } from "@/lib/middleware/admin"; import { getAllAiModels, getAiModelById, getAiModelsByProviderId, createAiModel, updateAiModel, deleteAiModel, getAiProviderById, } from "@/lib/db/queries"; import { ChatSDKError } from "@/lib/errors"; import { getAiModelAgentCodingModelId, isAiModelAgentAuto, isAiModelMultimodal, } from "@/lib/db/ai-model-flags"; import { buildVideoDurationCostMap, getConfiguredVideoDurationOptions, } from "@/lib/ai/video-model-config"; // ๐Ÿ†• ๅ›พ็‰‡็”Ÿๆˆๆจกๅž‹็‰นๆ€ง้…็ฝฎ Schema const imageGenConfigSchema = z.object({ maxImages: z.number().min(1).max(10).optional(), supportsSize: z.boolean().optional(), supports1K: z.boolean().optional(), supports2K: z.boolean().optional(), supports4K: z.boolean().optional(), supportsPromptExtend: z.boolean().optional(), supportsNegativePrompt: z.boolean().optional(), supportsWatermark: z.boolean().optional(), supportsSeed: z.boolean().optional(), creditCostPerImage: z.number().min(0).max(100000).optional(), creditCostPerVideo: z.number().min(0).max(100000).optional(), videoDurationOptions: z.array(z.number().min(1).max(600)).max(20).optional(), creditCostPerVideoByDuration: z.record(z.string(), z.number().min(0).max(100000)).optional(), enableLoadingPlaceholder: z.boolean().optional(), loadingColor: z.string().optional(), }).optional(); function normalizeImageGenConfig(config: z.infer) { if (!config) return config; const videoDurationOptions = getConfiguredVideoDurationOptions(config); const creditCostPerVideoByDuration = buildVideoDurationCostMap({ durationOptions: videoDurationOptions, costMap: config.creditCostPerVideoByDuration, fallbackCost: config.creditCostPerVideo, }); return { ...config, videoDurationOptions: videoDurationOptions.length ? videoDurationOptions : undefined, creditCostPerVideoByDuration, }; } const createModelSchema = z.object({ providerId: z.string().uuid(), modelId: z.string().min(1).max(128), displayName: z.string().min(1).max(128), avatarUrl: z.string().url().max(1000).optional().or(z.literal("")), description: z.string().optional(), supportsVision: z.boolean().optional(), supportsImageGen: z.boolean().optional(), isEnabled: z.boolean().optional(), isHidden: z.boolean().optional(), isDefault: z.boolean().optional(), sortOrder: z.number().optional(), fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), excludeFromFailover: z.boolean().optional(), isAgentModel: z.boolean().optional(), agentCodingModelId: z.string().max(128).optional(), isMultimodal: z.boolean().optional(), imageGenConfig: imageGenConfigSchema, // ๐Ÿ†• ๅ›พ็‰‡็”Ÿๆˆ็‰นๆ€ง้…็ฝฎ }); const updateModelSchema = z.object({ id: z.string().uuid(), providerId: z.string().uuid().optional(), modelId: z.string().min(1).max(128).optional(), displayName: z.string().min(1).max(128).optional(), avatarUrl: z.string().url().max(1000).optional().or(z.literal("")), description: z.string().optional(), supportsVision: z.boolean().optional(), supportsImageGen: z.boolean().optional(), isEnabled: z.boolean().optional(), isHidden: z.boolean().optional(), isDefault: z.boolean().optional(), sortOrder: z.number().optional(), fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), excludeFromFailover: z.boolean().optional(), isAgentModel: z.boolean().optional(), agentCodingModelId: z.string().max(128).optional(), isMultimodal: z.boolean().optional(), imageGenConfig: imageGenConfigSchema, // ๐Ÿ†• ๅ›พ็‰‡็”Ÿๆˆ็‰นๆ€ง้…็ฝฎ }); function normalizeModel(model: any) { return { ...model, isAgentModel: isAiModelAgentAuto(model), agentCodingModelId: getAiModelAgentCodingModelId(model) || "", isMultimodal: isAiModelMultimodal(model), }; } function sortModelsForDisplay( models: T[] ) { return [...models].sort((a, b) => { const leftOrder = Number.isFinite(a.sortOrder as number) ? Number(a.sortOrder) : 0; const rightOrder = Number.isFinite(b.sortOrder as number) ? Number(b.sortOrder) : 0; if (leftOrder !== rightOrder) return leftOrder - rightOrder; const leftName = String(a.displayName || ""); const rightName = String(b.displayName || ""); return leftName.localeCompare(rightName, "zh-Hans-CN"); }); } export async function GET(request: Request) { try { await requireAdmin(); const { searchParams } = new URL(request.url); const id = searchParams.get("id"); const providerId = searchParams.get("providerId"); if (id) { const model = await getAiModelById({ id }); if (!model) { return new ChatSDKError("bad_request:api", "Model not found").toResponse(); } return Response.json({ model: normalizeModel(model) }); } if (providerId) { const models = await getAiModelsByProviderId({ providerId }); return Response.json({ models: sortModelsForDisplay(models.map(normalizeModel)) }); } const models = await getAllAiModels(); return Response.json({ models: sortModelsForDisplay(models.map(normalizeModel)) }); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); } return new ChatSDKError("offline:admin").toResponse(); } } export async function POST(request: Request) { try { await requireAdmin(); const body = await request.json(); const data = createModelSchema.parse(body); // ้ชŒ่ฏ provider ๆ˜ฏๅฆๅญ˜ๅœจ const provider = await getAiProviderById({ id: data.providerId }); if (!provider) { return new ChatSDKError("bad_request:api", "Provider not found").toResponse(); } const model = await createAiModel({ ...data, imageGenConfig: normalizeImageGenConfig(data.imageGenConfig), }); return Response.json({ model: normalizeModel(model[0]) }); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); } if (error instanceof z.ZodError) { return new ChatSDKError("bad_request:api", error.message).toResponse(); } return new ChatSDKError("offline:admin").toResponse(); } } export async function PUT(request: Request) { try { await requireAdmin(); const body = await request.json(); console.log("[Admin API] Update model request body:", JSON.stringify(body)); // Debug log const data = updateModelSchema.parse(body); console.log("[Admin API] Validation passed, updating model..."); // Debug log const model = await updateAiModel({ ...data, imageGenConfig: normalizeImageGenConfig(data.imageGenConfig), }); if (model.length === 0) { console.error("[Admin API] Model not found:", data.id); // Debug log return new ChatSDKError("bad_request:api", "Model not found").toResponse(); } console.log("[Admin API] Model updated successfully:", model[0].id); // Debug log return Response.json({ model: normalizeModel(model[0]) }); } catch (error) { console.error("[Admin API] Update model error:", error); // Debug log if (error instanceof ChatSDKError) { return error.toResponse(); } if (error instanceof z.ZodError) { console.error("[Admin API] Validation error details:", error.errors); // Debug log return new ChatSDKError("bad_request:api", error.message).toResponse(); } return new ChatSDKError("offline:admin").toResponse(); } } export async function DELETE(request: Request) { try { await requireAdmin(); const { searchParams } = new URL(request.url); const id = searchParams.get("id"); if (!id) { return new ChatSDKError("bad_request:api", "Model ID is required").toResponse(); } await deleteAiModel({ id }); return Response.json({ success: true }); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); } return new ChatSDKError("offline:admin").toResponse(); } }