import { z } from "zod"; import { ACCOUNT_FALLBACK_STRATEGY_VALUES, ROUTING_STRATEGY_VALUES, } from "@/shared/constants/routingStrategies"; import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints"; import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; import { isForbiddenUpstreamHeaderName, isForbiddenCustomHeaderName, } from "@/shared/constants/upstreamHeaders"; import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; import { accessScheduleSchema } from "./misc.ts"; // ──── API Key Schemas ──── export const createKeySchema = z.object({ name: z.string().min(1, "Name is required").max(200), noLog: z.boolean().optional(), allowUsageCommand: z.boolean().optional(), usageLimitEnabled: z.boolean().optional(), dailyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(), weeklyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(), scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(), }); export const createSyncTokenSchema = z.object({ name: z.string().trim().min(1, "Name is required").max(200), }); export const setBudgetSchema = z.object({ apiKeyId: z.string().trim().min(1, "apiKeyId is required"), // #3537: a limit of 0 means "no limit for this period" (checkBudget only enforces when // activeLimitUsd > 0). The dashboard sends 0 for unfilled fields, so 0 must be accepted — // `.positive()` (rejects 0) used to 400 any save that left a field blank. Negatives are // still rejected by `.min(0)`. dailyLimitUsd: z.coerce.number().min(0, "dailyLimitUsd must be zero or greater").optional(), weeklyLimitUsd: z.coerce.number().min(0, "weeklyLimitUsd must be zero or greater").optional(), monthlyLimitUsd: z.coerce.number().min(0, "monthlyLimitUsd must be zero or greater").optional(), warningThreshold: z.coerce.number().min(0).max(1).optional(), resetInterval: z.enum(["daily", "weekly", "monthly"]).optional(), resetTime: z .string() .trim() .regex(/^\d{2}:\d{2}$/, "resetTime must be in HH:MM format") .optional(), }); export const setTokenLimitSchema = z .object({ id: z.string().trim().min(1).optional(), apiKeyId: z.string().trim().min(1, "apiKeyId is required"), scopeType: z.enum(["model", "provider", "global"]), scopeValue: z.string().trim().default(""), tokenLimit: z.coerce .number() .int("tokenLimit must be an integer") .positive("tokenLimit must be greater than zero"), resetInterval: z.enum(["daily", "weekly", "monthly"]).default("monthly"), resetTime: z .string() .trim() .regex(/^\d{2}:\d{2}$/, "resetTime must be in HH:MM format") .optional(), enabled: z.boolean().default(true), }) .superRefine((value, ctx) => { if (value.scopeType !== "global" && (!value.scopeValue || value.scopeValue.length === 0)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scopeValue is required unless scopeType is 'global'", path: ["scopeValue"], }); } }); export const updateKeyPermissionsSchema = z .object({ name: z.string().trim().min(1).max(200).optional(), allowedModels: z.array(z.string().trim().min(1)).max(1000).optional(), allowedCombos: z.array(z.string().trim().min(1).max(200)).max(500).optional(), allowedConnections: z.array(z.string().uuid()).max(100).optional(), noLog: z.boolean().optional(), autoResolve: z.boolean().optional(), isActive: z.boolean().optional(), throttleDelayMs: z.number().int().min(0).max(300000).optional(), isBanned: z.boolean().optional(), expiresAt: z.string().datetime().nullable().optional(), maxSessions: z.number().int().min(0).max(10000).optional(), accessSchedule: z.union([accessScheduleSchema, z.null()]).optional(), rateLimits: z .union([ z .array( z.object({ limit: z.number().int().positive(), window: z.number().int().positive() }) ) .max(50), z.null(), ]) .optional(), scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(), allowedEndpoints: z.array(z.string().trim().min(1).max(64)).max(20).optional(), streamDefaultMode: z.enum(["legacy", "json"]).optional(), disableNonPublicModels: z.boolean().optional(), allowUsageCommand: z.boolean().optional(), usageLimitEnabled: z.boolean().optional(), dailyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(), weeklyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(), }) .superRefine((value, ctx) => { if ( value.name === undefined && value.allowedModels === undefined && value.allowedCombos === undefined && value.allowedConnections === undefined && value.noLog === undefined && value.autoResolve === undefined && value.isActive === undefined && value.throttleDelayMs === undefined && value.isBanned === undefined && value.expiresAt === undefined && value.maxSessions === undefined && value.accessSchedule === undefined && value.rateLimits === undefined && value.scopes === undefined && value.allowedEndpoints === undefined && value.streamDefaultMode === undefined && value.disableNonPublicModels === undefined && value.allowUsageCommand === undefined && value.usageLimitEnabled === undefined && value.dailyUsageLimitUsd === undefined && value.weeklyUsageLimitUsd === undefined ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "No valid fields to update", path: [], }); } });