import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alertDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { resetDurationLabels } from "@/lib/constants/governance"; import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons"; import { ProviderLabels, ProviderName } from "@/lib/constants/logs"; import { getErrorMessage, useDeleteModelConfigMutation } from "@/lib/store"; import { ModelConfig } from "@/lib/types/governance"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/utils/governance"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { ChevronLeft, ChevronRight, Edit, Plus, Search, Trash2 } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import ModelLimitSheet from "./modelLimitSheet"; import { ModelLimitsEmptyState } from "./modelLimitsEmptyState"; // Helper to format reset duration for display const formatResetDuration = (duration: string) => { return resetDurationLabels[duration] || duration; }; const toTestIdPart = (value: string) => value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); interface ModelLimitsTableProps { modelConfigs: ModelConfig[]; totalCount: number; search: string; debouncedSearch: string; onSearchChange: (value: string) => void; offset: number; limit: number; onOffsetChange: (offset: number) => void; } export default function ModelLimitsTable({ modelConfigs, totalCount, search, debouncedSearch, onSearchChange, offset, limit, onOffsetChange, }: ModelLimitsTableProps) { const [showModelLimitSheet, setShowModelLimitSheet] = useState(false); const [editingModelConfigId, setEditingModelConfigId] = useState(null); // Derive editingModelConfig from props so it stays in sync with RTK cache updates const editingModelConfig = useMemo( () => (editingModelConfigId ? (modelConfigs.find((mc) => mc.id === editingModelConfigId) ?? null) : null), [editingModelConfigId, modelConfigs], ); const hasCreateAccess = useRbac(RbacResource.Governance, RbacOperation.Create); const hasUpdateAccess = useRbac(RbacResource.Governance, RbacOperation.Update); const hasDeleteAccess = useRbac(RbacResource.Governance, RbacOperation.Delete); const [deleteModelConfig, { isLoading: isDeleting }] = useDeleteModelConfigMutation(); const handleDelete = async (id: string) => { try { await deleteModelConfig(id).unwrap(); toast.success("Model limit deleted successfully"); } catch (error) { toast.error(getErrorMessage(error)); } }; const handleAddModelLimit = () => { setEditingModelConfigId(null); setShowModelLimitSheet(true); }; const handleEditModelLimit = (config: ModelConfig, e: React.MouseEvent) => { e.stopPropagation(); setEditingModelConfigId(config.id); setShowModelLimitSheet(true); }; const handleModelLimitSaved = () => { setShowModelLimitSheet(false); setEditingModelConfigId(null); }; const hasActiveFilters = debouncedSearch; // True empty state: no model limits at all (not just filtered to zero) if (totalCount === 0 && !hasActiveFilters) { return ( <> {showModelLimitSheet && ( setShowModelLimitSheet(false)} /> )} ); } return ( <> {showModelLimitSheet && ( setShowModelLimitSheet(false)} /> )}

Model Limits

Configure budgets and rate limits at the model level. For provider-specific limits, visit each provider's settings.

{/* Toolbar: Search */}
onSearchChange(e.target.value)} className="pl-9" data-testid="model-limits-search-input" />
Model Provider Budget Rate Limit {modelConfigs.length === 0 ? ( No matching model limits found. ) : ( modelConfigs.map((config) => { const isBudgetExhausted = config.budget?.max_limit && config.budget.max_limit > 0 && config.budget.current_usage >= config.budget.max_limit; const isRateLimitExhausted = (config.rate_limit?.token_max_limit && config.rate_limit.token_max_limit > 0 && config.rate_limit.token_current_usage >= config.rate_limit.token_max_limit) || (config.rate_limit?.request_max_limit && config.rate_limit.request_max_limit > 0 && config.rate_limit.request_current_usage >= config.rate_limit.request_max_limit); const isExhausted = isBudgetExhausted || isRateLimitExhausted; // Compute safe percentages to avoid division by zero const budgetPercentage = config.budget?.max_limit && config.budget.max_limit > 0 ? Math.min((config.budget.current_usage / config.budget.max_limit) * 100, 100) : 0; const tokenPercentage = config.rate_limit?.token_max_limit && config.rate_limit.token_max_limit > 0 ? Math.min((config.rate_limit.token_current_usage / config.rate_limit.token_max_limit) * 100, 100) : 0; const requestPercentage = config.rate_limit?.request_max_limit && config.rate_limit.request_max_limit > 0 ? Math.min((config.rate_limit.request_current_usage / config.rate_limit.request_max_limit) * 100, 100) : 0; return (
{config.model_name} {isExhausted && ( Limit Reached )}
{config.provider ? (
{ProviderLabels[config.provider as ProviderName] || config.provider}
) : ( All Providers )}
{config.budget ? (
{formatCurrency(config.budget.max_limit)} {formatResetDuration(config.budget.reset_duration)}
div]:bg-red-500/70" : budgetPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{formatCurrency(config.budget.current_usage)} / {formatCurrency(config.budget.max_limit)}

Resets {formatResetDuration(config.budget.reset_duration)}

) : ( - )}
{config.rate_limit ? (
{config.rate_limit.token_max_limit && (
{config.rate_limit.token_max_limit.toLocaleString()} tokens {formatResetDuration(config.rate_limit.token_reset_duration || "1h")}
= config.rate_limit.token_max_limit ? "[&>div]:bg-red-500/70" : tokenPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{config.rate_limit.token_current_usage.toLocaleString()} /{" "} {config.rate_limit.token_max_limit.toLocaleString()} tokens

Resets {formatResetDuration(config.rate_limit.token_reset_duration || "1h")}

)} {config.rate_limit.request_max_limit && (
{config.rate_limit.request_max_limit.toLocaleString()} req {formatResetDuration(config.rate_limit.request_reset_duration || "1h")}
= config.rate_limit.request_max_limit ? "[&>div]:bg-red-500/70" : requestPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{config.rate_limit.request_current_usage.toLocaleString()} /{" "} {config.rate_limit.request_max_limit.toLocaleString()} requests

Resets {formatResetDuration(config.rate_limit.request_reset_duration || "1h")}

)}
) : ( - )}
e.stopPropagation()}>
Delete Model Limit Are you sure you want to delete the limit for " {config.model_name.length > 30 ? `${config.model_name.slice(0, 30)}...` : config.model_name} "? This action cannot be undone. Cancel handleDelete(config.id)} disabled={isDeleting} className="bg-red-600 hover:bg-red-700" > {isDeleting ? "Deleting..." : "Delete"}
); }) )}
{/* Pagination */} {totalCount > 0 && (

Showing {offset + 1}-{Math.min(offset + limit, totalCount)} of {totalCount}

)}
); }