/** * POST /api/playground/simulate-route * * Simulates combo routing without executing the actual request. * Returns which targets would be tried, in which order, and with what expected outcome. */ import { NextResponse } from "next/server"; import { z } from "zod"; import { getCombos } from "@/lib/db/combos"; import { getProviderConnections } from "@/lib/db/providers"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; interface SimulateRequest { /** Combo ID to simulate */ comboId?: string; /** Combo config inline (mutually exclusive with comboId) */ combo?: { name: string; strategy: string; targets: Array<{ provider: string; model: string; weight?: number }>; }; /** Estimated prompt tokens */ promptTokens?: number; /** Task type hint */ taskType?: string; } interface TargetSimulation { provider: string; model: string; strategy: string; rank: number; estimatedCost: number; estimatedLatencyMs: number; status: "available" | "no_quota" | "degraded" | "error" | "unknown"; maxTokens?: number; contextWindow?: number; } interface SimulateResponse { comboId?: string; comboName: string; strategy: string; targets: TargetSimulation[]; totalEstimatedCost: number; totalEstimatedLatencyMs: number; warnings: string[]; errors: string[]; } const simulateRequestSchema = z .object({ comboId: z.string().trim().min(1).optional(), combo: z .object({ name: z.string().trim().min(1), strategy: z.string().trim().min(1), targets: z .array( z.object({ provider: z.string().trim().min(1), model: z.string().trim().min(1), weight: z.number().optional(), }) ) .min(1), }) .optional(), promptTokens: z.number().int().positive().optional(), taskType: z.string().optional(), }) .refine((value) => Boolean(value.comboId) !== Boolean(value.combo), { message: "Exactly one of comboId or combo is required", }); function estimateCost(model: string, promptTokens: number): number { // Rough cost estimates per model family const modelLower = model.toLowerCase(); const rate = modelLower.includes("sonnet") ? 3 : modelLower.includes("haiku") ? 0.25 : modelLower.includes("opus") ? 15 : modelLower.includes("gpt-4") ? 10 : modelLower.includes("gpt-3.5") ? 0.5 : modelLower.includes("gemini") ? 0.15 : modelLower.includes("claude") ? 3 : modelLower.includes("deepseek") ? 0.5 : modelLower.includes("mistral") ? 0.6 : modelLower.includes("llama") ? 0.3 : modelLower.includes("qwen") ? 0.4 : 1; return (rate * promptTokens) / 1_000_000; } function estimateLatency(model: string): number { const modelLower = model.toLowerCase(); if (modelLower.includes("haiku")) return 800; if (modelLower.includes("sonnet")) return 1500; if (modelLower.includes("opus")) return 4000; if (modelLower.includes("gpt-4")) return 2000; if (modelLower.includes("gpt-3.5")) return 500; if (modelLower.includes("gemini")) return 1200; if (modelLower.includes("deepseek")) return 3000; if (modelLower.includes("mistral")) return 1000; if (modelLower.includes("llama")) return 2500; if (modelLower.includes("qwen")) return 1800; if (modelLower.includes("claude")) return 1500; return 2000; } function estimateContextWindow(model: string): number { const modelLower = model.toLowerCase(); if (modelLower.includes("128k") || modelLower.includes("128")) return 128000; if (modelLower.includes("200k") || modelLower.includes("200")) return 200000; if (modelLower.includes("1m") || modelLower.includes("1m")) return 1000000; if (modelLower.includes("2m") || modelLower.includes("2m")) return 2000000; return 128000; } export async function POST(request: Request) { try { const rawBody = await request.json(); const validation = validateBody(simulateRequestSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const body: SimulateRequest = validation.data; const warnings: string[] = []; const errors: string[] = []; let comboInfo: { name: string; strategy: string; targets: Array<{ provider: string; model: string; weight?: number }>; } | null = null; // Resolve combo if (body.comboId) { const combos = (await getCombos()) as any[]; const combo = combos.find((c) => c.id === body.comboId || c.name === body.comboId); if (!combo) { errors.push(`Combo "${body.comboId}" not found.`); return NextResponse.json({ error: "Combo not found" }, { status: 404 }); } const targets = typeof combo.targets === "string" ? JSON.parse(combo.targets) : combo.targets; comboInfo = { name: combo.name, strategy: combo.strategy, targets }; } else if (body.combo) { comboInfo = body.combo; } else { errors.push("Either comboId or combo config is required."); return NextResponse.json({ error: "Missing combo configuration" }, { status: 400 }); } const promptTokens = body.promptTokens || 500; const targets: TargetSimulation[] = []; let totalCost = 0; let totalLatency = 0; // Get provider connections for status const connections = (await getProviderConnections({})) as any[]; for (let i = 0; i < comboInfo.targets.length; i++) { const t = comboInfo.targets[i]; const conn = connections.find( (c: any) => c.id === t.provider || c.name === t.provider || c.displayName === t.provider ); const cost = estimateCost(t.model, promptTokens); const latency = estimateLatency(t.model); totalCost += cost; totalLatency += latency; // Determine provider status let status: TargetSimulation["status"] = "available"; if (!conn) { status = "unknown"; warnings.push(`Provider "${t.provider}" not configured — will likely fail.`); } else if (!conn.isActive) { status = "error"; warnings.push(`Provider "${t.provider}" is inactive.`); } targets.push({ provider: t.provider, model: t.model, strategy: comboInfo.strategy, rank: i + 1, estimatedCost: cost, estimatedLatencyMs: latency, status, contextWindow: estimateContextWindow(t.model), }); } // Add strategy-specific warnings if (comboInfo.strategy === "priority" && targets.length > 1) { warnings.push("Priority strategy: targets after rank 1 only used if earlier targets fail."); } // Build response const response: SimulateResponse = { comboId: body.comboId, comboName: comboInfo.name, strategy: comboInfo.strategy, targets, totalEstimatedCost: totalCost, totalEstimatedLatencyMs: totalLatency, warnings, errors, }; return NextResponse.json(response, { status: 200 }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return NextResponse.json({ error: `Simulation error: ${message}` }, { status: 500 }); } }