155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
|
import { z } from "zod";
|
|
import { prisma } from "~/db.server";
|
|
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
|
|
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
|
|
|
export async function loader({ request }: LoaderFunctionArgs) {
|
|
await requireAdminApiRequest(request);
|
|
|
|
const url = new URL(request.url);
|
|
const page = parseInt(url.searchParams.get("page") ?? "1");
|
|
const pageSize = parseInt(url.searchParams.get("pageSize") ?? "50");
|
|
|
|
const [models, total] = await Promise.all([
|
|
prisma.llmModel.findMany({
|
|
where: { projectId: null },
|
|
include: {
|
|
pricingTiers: {
|
|
include: { prices: true },
|
|
orderBy: { priority: "asc" },
|
|
},
|
|
},
|
|
orderBy: { modelName: "asc" },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
}),
|
|
prisma.llmModel.count({ where: { projectId: null } }),
|
|
]);
|
|
|
|
return json({ models, total, page, pageSize });
|
|
}
|
|
|
|
const CreateModelSchema = z.object({
|
|
modelName: z.string().min(1),
|
|
matchPattern: z.string().min(1),
|
|
startDate: z.string().optional(),
|
|
source: z.enum(["default", "admin"]).optional().default("admin"),
|
|
provider: z.string().optional(),
|
|
description: z.string().optional(),
|
|
contextWindow: z.number().int().optional(),
|
|
maxOutputTokens: z.number().int().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
isHidden: z.boolean().optional(),
|
|
pricingUnit: z.string().optional(),
|
|
pricingTiers: z.array(
|
|
z.object({
|
|
name: z.string().min(1),
|
|
isDefault: z.boolean().default(true),
|
|
priority: z.number().int().default(0),
|
|
conditions: z
|
|
.array(
|
|
z.object({
|
|
usageDetailPattern: z.string(),
|
|
operator: z.enum(["gt", "gte", "lt", "lte", "eq", "neq"]),
|
|
value: z.number(),
|
|
})
|
|
)
|
|
.default([]),
|
|
prices: z.record(z.string(), z.number()),
|
|
})
|
|
),
|
|
});
|
|
|
|
export async function action({ request }: ActionFunctionArgs) {
|
|
await requireAdminApiRequest(request);
|
|
|
|
if (request.method !== "POST") {
|
|
return json({ error: "Method not allowed" }, { status: 405 });
|
|
}
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
}
|
|
|
|
const parsed = CreateModelSchema.safeParse(body);
|
|
|
|
if (!parsed.success) {
|
|
return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 });
|
|
}
|
|
|
|
const {
|
|
modelName,
|
|
matchPattern,
|
|
startDate,
|
|
source,
|
|
pricingTiers,
|
|
provider,
|
|
description,
|
|
contextWindow,
|
|
maxOutputTokens,
|
|
capabilities,
|
|
isHidden,
|
|
pricingUnit,
|
|
} = parsed.data;
|
|
|
|
// Validate regex pattern — strip (?i) POSIX flag since our registry handles it
|
|
try {
|
|
const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern;
|
|
new RegExp(testPattern);
|
|
} catch {
|
|
return json({ error: "Invalid regex in matchPattern" }, { status: 400 });
|
|
}
|
|
|
|
// Create model + tiers atomically
|
|
const created = await prisma.$transaction(async (tx) => {
|
|
const model = await tx.llmModel.create({
|
|
data: {
|
|
friendlyId: generateFriendlyId("llm_model"),
|
|
modelName,
|
|
matchPattern,
|
|
startDate: startDate ? new Date(startDate) : null,
|
|
source,
|
|
provider: provider ?? null,
|
|
description: description ?? null,
|
|
contextWindow: contextWindow ?? null,
|
|
maxOutputTokens: maxOutputTokens ?? null,
|
|
capabilities: capabilities ?? [],
|
|
isHidden: isHidden ?? false,
|
|
pricingUnit: pricingUnit ?? null,
|
|
},
|
|
});
|
|
|
|
for (const tier of pricingTiers) {
|
|
await tx.llmPricingTier.create({
|
|
data: {
|
|
modelId: model.id,
|
|
name: tier.name,
|
|
isDefault: tier.isDefault,
|
|
priority: tier.priority,
|
|
conditions: tier.conditions,
|
|
prices: {
|
|
create: Object.entries(tier.prices).map(([usageType, price]) => ({
|
|
modelId: model.id,
|
|
usageType,
|
|
price,
|
|
})),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return tx.llmModel.findUnique({
|
|
where: { id: model.id },
|
|
include: {
|
|
pricingTiers: { include: { prices: true } },
|
|
},
|
|
});
|
|
});
|
|
|
|
return json({ model: created }, { status: 201 });
|
|
}
|