mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(api): validate pricing sync and task routing routes
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user