mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(quota): add canonical dimensions, types and Zod schemas
This commit is contained in:
61
src/lib/quota/dimensions.ts
Normal file
61
src/lib/quota/dimensions.ts
Normal 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
57
src/lib/quota/types.ts
Normal 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 };
|
||||
}
|
||||
46
src/shared/schemas/quota.ts
Normal file
46
src/shared/schemas/quota.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user