fix(api): validate pricing sync and task routing routes

This commit is contained in:
Kfir Amar
2026-03-15 19:33:35 +02:00
parent b1c713de60
commit fc2af8ba87
4 changed files with 161 additions and 13 deletions

View File

@@ -7,14 +7,31 @@
*/
import { NextRequest, NextResponse } from "next/server";
import { pricingSyncRequestSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
export async function POST(request: NextRequest) {
let rawBody: unknown;
try {
const body = await request.json().catch(() => ({}));
const sources = Array.isArray(body.sources)
? body.sources.filter((s: unknown): s is string => typeof s === "string")
: undefined;
const dryRun = body.dryRun === true;
rawBody = await request.json();
} catch {
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
try {
const validation = validateBody(pricingSyncRequestSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { sources, dryRun = false } = validation.data;
const { syncPricingFromSources } = await import("@/lib/pricingSync");
const result = await syncPricingFromSources({ sources, dryRun });

View File

@@ -6,6 +6,8 @@ import {
getDefaultTaskModelMap,
} from "@omniroute/open-sse/services/taskAwareRouter.ts";
import { updateSettings } from "@/lib/db/settings";
import { taskRoutingActionSchema, updateTaskRoutingSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
/**
* GET /api/settings/task-routing
@@ -29,15 +31,29 @@ export async function GET() {
* Body: { enabled?: boolean, taskModelMap?: { coding?: "...", ... }, detectionEnabled?: boolean }
*/
export async function PUT(request: Request) {
let rawBody: Record<string, unknown>;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json({ error: { message: "Invalid JSON body" } }, { status: 400 });
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
try {
setTaskRoutingConfig(rawBody as any);
const validation = validateBody(updateTaskRoutingSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const config = validation.data;
setTaskRoutingConfig(config);
// Persist to database (excluding stats)
const { stats, ...persistable } = getTaskRoutingConfig();
@@ -56,15 +72,29 @@ export async function PUT(request: Request) {
* For "detect": pass { action: "detect", body: <request-body> } to test detection
*/
export async function POST(request: Request) {
let rawBody: any;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json({ error: { message: "Invalid JSON body" } }, { status: 400 });
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
try {
if (rawBody.action === "reset-stats") {
const validation = validateBody(taskRoutingActionSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const actionRequest = validation.data;
if (actionRequest.action === "reset-stats") {
resetTaskRoutingStats();
return NextResponse.json({
success: true,
@@ -72,9 +102,9 @@ export async function POST(request: Request) {
});
}
if (rawBody.action === "detect") {
if (actionRequest.action === "detect") {
const { detectTaskType } = await import("@omniroute/open-sse/services/taskAwareRouter.ts");
const taskType = detectTaskType(rawBody.body || {});
const taskType = detectTaskType(actionRequest.body || {});
const config = getTaskRoutingConfig();
return NextResponse.json({
taskType,

View File

@@ -378,6 +378,58 @@ export const resetStatsActionSchema = z.object({
action: z.literal("reset-stats"),
});
const pricingSyncSourceSchema = z.enum(["litellm"]);
export const pricingSyncRequestSchema = z
.object({
sources: z.array(pricingSyncSourceSchema).min(1).optional(),
dryRun: z.boolean().optional(),
})
.strict();
const taskRoutingModelMapSchema = z
.object({
coding: z.string().max(200).optional(),
creative: z.string().max(200).optional(),
analysis: z.string().max(200).optional(),
vision: z.string().max(200).optional(),
summarization: z.string().max(200).optional(),
background: z.string().max(200).optional(),
chat: z.string().max(200).optional(),
})
.strict();
export const updateTaskRoutingSchema = z
.object({
enabled: z.boolean().optional(),
taskModelMap: taskRoutingModelMapSchema.optional(),
detectionEnabled: z.boolean().optional(),
})
.strict()
.superRefine((value, ctx) => {
if (
value.enabled === undefined &&
value.taskModelMap === undefined &&
value.detectionEnabled === undefined
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
export const taskRoutingActionSchema = z.discriminatedUnion("action", [
resetStatsActionSchema,
z
.object({
action: z.literal("detect"),
body: jsonObjectSchema.optional(),
})
.strict(),
]);
export const updateComboDefaultsSchema = z
.object({
comboDefaults: comboRuntimeConfigSchema.optional(),

View File

@@ -10,6 +10,9 @@ import {
v1EmbeddingsSchema,
providerChatCompletionSchema,
v1CountTokensSchema,
pricingSyncRequestSchema,
updateTaskRoutingSchema,
taskRoutingActionSchema,
} from "../../src/shared/validation/schemas.ts";
test("translatorDetectSchema rejects empty body object", () => {
@@ -130,3 +133,49 @@ test("v1CountTokensSchema rejects empty messages", () => {
});
assert.equal(validation.success, false);
});
test("pricingSyncRequestSchema rejects unsupported sources", () => {
const validation = validateBody(pricingSyncRequestSchema, {
sources: ["unknown-source"],
});
assert.equal(validation.success, false);
});
test("pricingSyncRequestSchema accepts dryRun-only requests", () => {
const validation = validateBody(pricingSyncRequestSchema, {
dryRun: true,
});
assert.equal(validation.success, true);
});
test("updateTaskRoutingSchema rejects empty payloads", () => {
const validation = validateBody(updateTaskRoutingSchema, {});
assert.equal(validation.success, false);
});
test("updateTaskRoutingSchema accepts partial task routing updates", () => {
const validation = validateBody(updateTaskRoutingSchema, {
enabled: true,
taskModelMap: {
coding: "codex/gpt-5.1-codex",
},
});
assert.equal(validation.success, true);
});
test("taskRoutingActionSchema rejects unknown actions", () => {
const validation = validateBody(taskRoutingActionSchema, {
action: "noop",
});
assert.equal(validation.success, false);
});
test("taskRoutingActionSchema accepts detect action with object body", () => {
const validation = validateBody(taskRoutingActionSchema, {
action: "detect",
body: {
messages: [{ role: "user", content: "write code" }],
},
});
assert.equal(validation.success, true);
});