Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
7.51 kB
/**
* 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 });
}
}