feat(quota): add canonical dimensions, types and Zod schemas

This commit is contained in:
diegosouzapw
2026-05-27 19:04:55 -03:00
parent 722e9f41cd
commit 70b9cc7831
3 changed files with 164 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { z } from "zod";
export const QuotaUnitSchema = z.enum(["percent", "requests", "tokens", "usd"]);
export type QuotaUnit = z.infer<typeof QuotaUnitSchema>;
export const QuotaWindowSchema = z.enum(["5h", "hourly", "daily", "weekly", "monthly"]);
export type QuotaWindow = z.infer<typeof QuotaWindowSchema>;
export const PolicySchema = z.enum(["hard", "soft", "burst"]);
export type Policy = z.infer<typeof PolicySchema>;
export const QuotaDimensionSchema = z.object({
unit: QuotaUnitSchema,
window: QuotaWindowSchema,
limit: z.number().positive(),
});
export type QuotaDimension = z.infer<typeof QuotaDimensionSchema>;
export const PoolAllocationSchema = z.object({
apiKeyId: z.string().min(1),
weight: z.number().min(0).max(100),
capValue: z.number().positive().optional(),
capUnit: QuotaUnitSchema.optional(),
policy: PolicySchema,
});
export type PoolAllocation = z.infer<typeof PoolAllocationSchema>;
export const ProviderPlanSchema = z.object({
connectionId: z.string().nullable(),
provider: z.string().min(1),
dimensions: z.array(QuotaDimensionSchema).min(1),
source: z.enum(["auto", "manual"]),
});
export type ProviderPlan = z.infer<typeof ProviderPlanSchema>;
export const QuotaPoolSchema = z.object({
id: z.string().min(1),
connectionId: z.string().min(1),
name: z.string().min(1),
createdAt: z.string().datetime(),
allocations: z.array(PoolAllocationSchema).default([]),
});
export type QuotaPool = z.infer<typeof QuotaPoolSchema>;
export interface DimensionKey {
poolId: string;
unit: QuotaUnit;
window: QuotaWindow;
}
export const WINDOW_MS: Record<QuotaWindow, number> = {
hourly: 60 * 60 * 1000,
"5h": 5 * 60 * 60 * 1000,
daily: 24 * 60 * 60 * 1000,
weekly: 7 * 24 * 60 * 60 * 1000,
monthly: 30 * 24 * 60 * 60 * 1000,
};
export function dimensionKeyToString(k: DimensionKey): string {
return `${k.poolId}:${k.unit}:${k.window}`;
}

57
src/lib/quota/types.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { DimensionKey, Policy, QuotaDimension } from "./dimensions";
export interface PoolUsageSnapshot {
poolId: string;
generatedAt: string;
dimensions: Array<{
unit: QuotaDimension["unit"];
window: QuotaDimension["window"];
limit: number;
consumedTotal: number;
perKey: Array<{
apiKeyId: string;
consumed: number;
fairShare: number;
deficit: number;
borrowing: boolean;
}>;
}>;
burnRate?: {
tokensPerSecond: number;
timeToExhaustionMs: number | null;
};
}
export interface ConsumeResult {
effective: number;
limit: number;
fairShare: number;
allowed: boolean;
policyApplied: Policy;
reason: "ok" | "fair-share" | "cap-absolute" | "global-saturated";
}
export interface QuotaStore {
consume(apiKeyId: string, dim: DimensionKey, cost: number): Promise<number>;
peek(apiKeyId: string, dim: DimensionKey): Promise<number>;
poolUsage(poolId: string): Promise<PoolUsageSnapshot>;
clear(apiKeyId: string, dim: DimensionKey): Promise<void>;
}
export interface EnforceInput {
apiKeyId: string;
connectionId: string;
provider: string;
estimatedCost?: { tokens?: number; usd?: number; requests?: number };
}
export type EnforceDecision =
| { kind: "allow"; deprioritize?: boolean }
| { kind: "block"; reason: string; httpStatus: 429; retryAfterSeconds?: number };
export interface RecordConsumptionInput {
apiKeyId: string;
connectionId: string;
provider: string;
cost: { tokens?: number; usd?: number; requests?: number };
}

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
import { PoolAllocationSchema, QuotaDimensionSchema } from "@/lib/quota/dimensions";
export const PoolCreateSchema = z.object({
connectionId: z.string().min(1),
name: z.string().min(1).max(120),
allocations: z.array(PoolAllocationSchema).default([]),
});
export type PoolCreate = z.infer<typeof PoolCreateSchema>;
export const PoolUpdateSchema = z.object({
name: z.string().min(1).max(120).optional(),
allocations: z.array(PoolAllocationSchema).optional(),
});
export type PoolUpdate = z.infer<typeof PoolUpdateSchema>;
export const PlanUpsertSchema = z.object({
dimensions: z.array(QuotaDimensionSchema).min(1),
});
export type PlanUpsert = z.infer<typeof PlanUpsertSchema>;
export const QuotaStoreSettingsSchema = z.object({
driver: z.enum(["sqlite", "redis"]),
redisUrl: z.string().url().nullable().optional(),
});
export type QuotaStoreSettings = z.infer<typeof QuotaStoreSettingsSchema>;
export const QuotaPreviewQuerySchema = z.object({
apiKeyId: z.string().min(1),
poolId: z.string().min(1),
estimatedTokens: z.coerce.number().nonnegative().optional(),
estimatedUsd: z.coerce.number().nonnegative().optional(),
estimatedRequests: z.coerce.number().int().nonnegative().optional(),
});
export type QuotaPreviewQuery = z.infer<typeof QuotaPreviewQuerySchema>;
export const AuditLogQuerySchema = z.object({
action: z.string().optional(),
actor: z.string().optional(),
level: z.enum(["high", "all"]).default("all"),
from: z.string().datetime().optional(),
to: z.string().datetime().optional(),
limit: z.coerce.number().int().min(1).max(500).default(50),
offset: z.coerce.number().int().min(0).max(10_000).default(0),
});
export type AuditLogQuery = z.infer<typeof AuditLogQuerySchema>;