From f39adbc50b6c8a45d417e632d6b05ac68b07b102 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 2 Mar 2026 19:05:56 -0800 Subject: [PATCH] SAVE --- .../routing_groups/DeploymentSelector.tsx | 150 ---- .../RoutingFlowVisualization.tsx | 350 ++++++++++ .../routing_groups/RoutingGroupBuilder.tsx | 657 +++++++++--------- .../routing_groups/RoutingGroupsTable.tsx | 277 ++++---- .../routing_groups/RoutingGroupsView.tsx | 281 ++++++-- 5 files changed, 1049 insertions(+), 666 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx create mode 100644 ui/litellm-dashboard/src/components/routing_groups/RoutingFlowVisualization.tsx diff --git a/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx b/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx deleted file mode 100644 index d72b971f33c..00000000000 --- a/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client"; - -import React, { useEffect, useMemo } from "react"; -import { Select } from "antd"; -import { RobotOutlined } from "@ant-design/icons"; -import { TextInput } from "@tremor/react"; -import { useRef, useState } from "react"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"; -import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; -import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer"; - -export interface DeploymentOption { - model_id: string; - model_name: string; - provider: string; -} - -interface DeploymentSelectorProps { - value?: string | null; - placeholder?: string; - onChange?: (deployment: DeploymentOption | null) => void; - disabled?: boolean; - style?: React.CSSProperties; - className?: string; - showLabel?: boolean; - labelText?: string; - excludeIds?: Set; -} - -export default function DeploymentSelector({ - value, - placeholder = "Select a deployment", - onChange, - disabled = false, - style, - className, - showLabel = true, - labelText = "Select Model", - excludeIds = new Set(), -}: DeploymentSelectorProps) { - const { accessToken, userId, userRole } = useAuthorized(); - const { data: rawModelData } = useModelsInfo(1, 200); - const { data: modelCostMapData } = useModelCostMap(); - const [selectedValue, setSelectedValue] = useState(value ?? undefined); - const [showCustomModelInput, setShowCustomModelInput] = useState(false); - const customModelTimeout = useRef(null); - - useEffect(() => { - setSelectedValue(value ?? undefined); - }, [value]); - - const getProviderFromModel = (model: string) => { - if (modelCostMapData && typeof modelCostMapData === "object" && model in modelCostMapData) { - return modelCostMapData[model]?.litellm_provider ?? "openai"; - } - return "openai"; - }; - - const modelData = useMemo(() => { - if (!rawModelData?.data) return { data: [] }; - return transformModelData(rawModelData, getProviderFromModel); - }, [rawModelData?.data, modelCostMapData]); - - const deploymentOptions = useMemo(() => { - if (!modelData?.data) return []; - return modelData.data - .filter((m: any) => m.model_info?.id && !excludeIds.has(m.model_info.id)) - .map((m: any) => ({ - model_id: m.model_info.id, - model_name: m.model_name, - provider: m.provider || "-", - label: `${m.provider || "Unknown"} / ${m.model_name}`, - })); - }, [modelData?.data, excludeIds]); - - const options = [ - { value: "__custom__", label: "Enter custom model", key: "custom" }, - ...deploymentOptions.map((d) => ({ - value: d.model_id, - label: d.label, - key: d.model_id, - })), - ]; - - const handleChange = (val: string) => { - if (val === "__custom__") { - setShowCustomModelInput(true); - setSelectedValue(undefined); - onChange?.(null); - } else { - setShowCustomModelInput(false); - setSelectedValue(val); - const dep = deploymentOptions.find((d) => d.model_id === val); - if (dep) { - onChange?.({ model_id: dep.model_id, model_name: dep.model_name, provider: dep.provider }); - } - } - }; - - const isCustomValue = !!value && !deploymentOptions.some((d) => d.model_id === value); - - const handleCustomModelChange = (inputVal: string) => { - if (customModelTimeout.current) clearTimeout(customModelTimeout.current); - customModelTimeout.current = setTimeout(() => { - if (inputVal.trim()) { - setSelectedValue(`custom:${inputVal}`); - onChange?.({ - model_id: inputVal.trim(), - model_name: inputVal.trim(), - provider: "custom", - }); - } else { - onChange?.(null); - } - }, 500); - }; - - return ( -
- {showLabel && ( -
- {labelText} -
- )} - setLogicalName(e.target.value)} - placeholder="prod-model" - className="flex-1 border-none outline-none text-sm font-mono text-gray-800 bg-transparent" - /> + {/* Weight distribution bar */} + {strategy === "weighted" && deployments.length > 0 && ( +
+
+ {deployments.map((dep, i) => { + const pct = totalWeight > 0 ? ((dep.weight ?? 1) / totalWeight) * 100 : 0; + return
; + })} +
+
+ {deployments.map((dep, i) => { + const pct = totalWeight > 0 ? Math.round(((dep.weight ?? 1) / totalWeight) * 100) : 0; + const shortName = dep.model_name.split("/").pop() || dep.model_name; + return ( +
+
+ {shortName}: {pct}% +
+ ); + })} +
+
+ )} + +
+ {deployments.map((dep, idx) => { + const pct = totalWeight > 0 ? Math.round(((dep.weight ?? 1) / totalWeight) * 100) : 0; + return ( +
handleDragStart(idx)} + onDragEnter={() => handleDragEnter(idx)} + onDragEnd={handleDragEnd} + onDragOver={(e) => e.preventDefault()} + className="rounded-lg border border-gray-200 bg-white px-3 py-2.5 cursor-grab active:cursor-grabbing hover:border-gray-300 transition-colors" + > +
+ + {idx + 1} +
e.stopPropagation()}> + handleDeploymentChange(idx, model)} + /> +
+ +
+ {strategy === "weighted" && ( +
e.stopPropagation()}> + Weight + updateWeight(idx, val)} + addonAfter="%" + style={{ width: 90 }} + /> + = {pct}% traffic +
+ )} +
+ ); + })} + + {Array.from({ length: emptySlots }).map((_, slotIdx) => ( +
+
+ {deployments.length + slotIdx + 1} +
+ handleSlotSelected(slotIdx, model)} + /> +
+
+
+ ))} +
+
+ + {/* 3 · Logical Model Name */} +
+
3 · Logical Model Name
+
What callers use to invoke this group
+
+ + setLogicalName(e.target.value)} + placeholder="prod-model" + className="flex-1 border-none outline-none text-sm font-mono text-gray-800 bg-transparent" + /> +
+
+
{codeSnippet}
+
+
+ +
- {/* Code snippet */} -
-
{codeSnippet}
-
-
+ {/* ── RIGHT: Light Flow Preview ── */} +
+ {/* Header */} +
+ Flow Preview +
+
+ {["1x", "2x", "5x"].map((label) => ( + + ))} +
+ + +
+
- {/* Save button */} -
- + {/* Flow canvas */} +
+
+ +
+
+ + {/* Stats footer */} +
+
+ {[ + { label: "REQUESTS", value: "0" }, + { label: "SUCCESS", value: "100%", color: "#22c55e" }, + { label: "AVG LATENCY", value: "—" }, + { label: "FALLBACKS", value: "0" }, + ].map((stat, i) => ( +
0 ? "1px solid #e5e7eb" : "none", paddingLeft: i > 0 ? 16 : 0 }}> +
{stat.label}
+
{stat.value}
+
+ ))} +
+
+ + + + Request Log +
+
+
); diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx index 7d12514d5c8..f8754928161 100644 --- a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx +++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx @@ -1,26 +1,28 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { Button, Popconfirm, Table, Tag, Typography } from "antd"; -import { DeleteOutlined, EditOutlined, ThunderboltOutlined } from "@ant-design/icons"; -import { routingGroupDeleteCall, routingGroupListCall } from "@/components/networking"; +import React from "react"; +import { Badge } from "@tremor/react"; +import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; +import { Button, Popconfirm, Tooltip } from "antd"; +import { DeleteOutlined, EditOutlined } from "@ant-design/icons"; -interface RoutingGroupsTableProps { - accessToken: string | null; - refreshKey: number; - onEdit: (group: Record) => void; - onTest?: (group: Record) => void; +interface RoutingGroupRow { + routing_group_id: string; + routing_group_name: string; + routing_strategy: string; + deployments: unknown[]; + is_active: boolean; + created_at: string; + description?: string; + [key: string]: unknown; } -const strategyColors: Record = { - "priority-failover": "blue", - weighted: "green", - "cost-based-routing": "gold", - "latency-based-routing": "cyan", - "least-busy": "purple", - "usage-based-routing-v2": "orange", - "simple-shuffle": "geekblue", -}; +interface RoutingGroupsTableProps { + data: RoutingGroupRow[]; + loading: boolean; + onEdit: (group: Record) => void; + onDelete: (groupId: string) => void; +} const strategyLabels: Record = { "priority-failover": "Priority Failover", @@ -32,137 +34,120 @@ const strategyLabels: Record = { "simple-shuffle": "Round Robin", }; +const strategyColors: Record = { + "priority-failover": "blue", + weighted: "emerald", + "cost-based-routing": "amber", + "latency-based-routing": "cyan", + "least-busy": "violet", + "usage-based-routing-v2": "orange", + "simple-shuffle": "indigo", +}; + export default function RoutingGroupsTable({ - accessToken, - refreshKey, + data, + loading, onEdit, - onTest, + onDelete, }: RoutingGroupsTableProps) { - const [groups, setGroups] = useState[]>([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (!accessToken) return; - setLoading(true); - routingGroupListCall(accessToken, 1, 50) - .then((resp) => { - setGroups(resp.routing_groups || []); - }) - .catch(console.error) - .finally(() => setLoading(false)); - }, [accessToken, refreshKey]); - - const handleDelete = async (groupId: string) => { - if (!accessToken) return; - try { - await routingGroupDeleteCall(accessToken, groupId); - setGroups((prev) => - prev.filter((g) => (g.routing_group_id as string) !== groupId) - ); - } catch (err) { - console.error("Failed to delete routing group:", err); - } - }; - - const columns = [ - { - title: "Name", - dataIndex: "routing_group_name", - key: "routing_group_name", - render: (name: string) => ( - {name} - ), - }, - { - title: "Strategy", - dataIndex: "routing_strategy", - key: "routing_strategy", - render: (strategy: string) => { - const label = strategyLabels[strategy] ?? strategy; - const color = strategyColors[strategy] ?? "default"; - return {label}; - }, - }, - { - title: "Deployments", - dataIndex: "deployments", - key: "deployments", - render: (deployments: unknown) => { - const count = Array.isArray(deployments) ? deployments.length : 0; - return {count}; - }, - }, - { - title: "Status", - dataIndex: "status", - key: "status", - render: (status: string) => { - const isActive = !status || status === "active"; - return ( - - {isActive ? "Active" : "Inactive"} - - ); - }, - }, - { - title: "Created", - dataIndex: "created_at", - key: "created_at", - render: (created: string) => { - if (!created) return "-"; - return new Date(created).toLocaleDateString(); - }, - }, - { - title: "Actions", - key: "actions", - render: (_: unknown, record: Record) => { - const groupId = record.routing_group_id as string; - return ( -
- {onTest && ( - - )} - - handleDelete(groupId)} - okText="Delete" - okButtonProps={{ danger: true }} - cancelText="Cancel" - > - - -
- ); - }, - }, - ]; - return ( - (record.routing_group_id as string) ?? Math.random().toString()} - pagination={{ pageSize: 20 }} - /> +
+
+
+ + + Name + Strategy + Deployments + Status + Created At + + + + + {loading ? ( + + +
Loading routing groups...
+
+
+ ) : data.length === 0 ? ( + + +
No routing groups yet. Create one to get started.
+
+
+ ) : ( + data.map((row) => { + const strat = row.routing_strategy; + const stratLabel = strategyLabels[strat] ?? strat; + const stratColor = strategyColors[strat] ?? "gray"; + const deps = row.deployments; + const depCount = Array.isArray(deps) ? deps.length : 0; + const active = row.is_active !== false; + const created = row.created_at; + + return ( + onEdit(row)} + > + + + {row.routing_group_name} + + + + {stratLabel} + + + {depCount} model{depCount !== 1 ? "s" : ""} + + + + {active ? "Active" : "Inactive"} + + + + {created ? ( + {new Date(created).toLocaleDateString()} + ) : ( + - + )} + + +
e.stopPropagation()}> + +
+
+
+ ); + }) + )} +
+
+ + ); } diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx index f82d770c4e0..6337535a944 100644 --- a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx +++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx @@ -1,11 +1,22 @@ "use client"; -import React, { useState } from "react"; -import { Button, Divider, Typography } from "antd"; -import { PlusOutlined, CloseOutlined } from "@ant-design/icons"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Button, Skeleton, Select } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; import RoutingGroupsTable from "./RoutingGroupsTable"; import RoutingGroupBuilder from "./RoutingGroupBuilder"; -import LiveTester from "./LiveTester"; +import { routingGroupListCall, routingGroupDeleteCall } from "@/components/networking"; + +interface RoutingGroupRow { + routing_group_id: string; + routing_group_name: string; + routing_strategy: string; + deployments: unknown[]; + is_active: boolean; + created_at: string; + description?: string; + [key: string]: unknown; +} interface RoutingGroupsViewProps { accessToken: string | null; @@ -13,6 +24,17 @@ interface RoutingGroupsViewProps { userId: string; } +const STRATEGY_OPTIONS = [ + { value: "all", label: "All Strategies" }, + { value: "priority-failover", label: "Priority Failover" }, + { value: "weighted", label: "Weighted" }, + { value: "cost-based-routing", label: "Cost-Based" }, + { value: "latency-based-routing", label: "Latency-Based" }, + { value: "least-busy", label: "Least Busy" }, + { value: "usage-based-routing-v2", label: "Usage-Based" }, + { value: "simple-shuffle", label: "Round Robin" }, +]; + export default function RoutingGroupsView({ accessToken, userRole, @@ -21,16 +43,82 @@ export default function RoutingGroupsView({ const [showBuilder, setShowBuilder] = useState(false); const [editTarget, setEditTarget] = useState | null>(null); const [refreshKey, setRefreshKey] = useState(0); - const [selectedGroup, setSelectedGroup] = useState | null>(null); + const [groups, setGroups] = useState([]); + const [loading, setLoading] = useState(false); + const [nameSearch, setNameSearch] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [selectedStrategy, setSelectedStrategy] = useState("all"); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 50; + + const searchTimerRef = useRef | null>(null); + const [debouncedSearch, setDebouncedSearch] = useState(""); + + useEffect(() => { + if (searchTimerRef.current) clearTimeout(searchTimerRef.current); + searchTimerRef.current = setTimeout(() => { + setDebouncedSearch(nameSearch); + }, 200); + return () => { + if (searchTimerRef.current) clearTimeout(searchTimerRef.current); + }; + }, [nameSearch]); + + useEffect(() => { + if (!accessToken) return; + setLoading(true); + routingGroupListCall(accessToken, 1, 500) + .then((resp) => { + setGroups((resp.routing_groups as RoutingGroupRow[]) || []); + }) + .catch(() => { + setGroups([]); + }) + .finally(() => setLoading(false)); + }, [accessToken, refreshKey]); + + const filteredData = useMemo(() => { + return groups.filter((g) => { + const nameMatch = + !debouncedSearch || + g.routing_group_name.toLowerCase().includes(debouncedSearch.toLowerCase()); + const strategyMatch = + selectedStrategy === "all" || g.routing_strategy === selectedStrategy; + return nameMatch && strategyMatch; + }); + }, [groups, debouncedSearch, selectedStrategy]); + + const totalCount = filteredData.length; + const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); + const paginatedData = filteredData.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize + ); + + useEffect(() => { + setCurrentPage(1); + }, [selectedStrategy, debouncedSearch]); + + const resetFilters = () => { + setNameSearch(""); + setDebouncedSearch(""); + setSelectedStrategy("all"); + setCurrentPage(1); + }; + + const handleDelete = async (groupId: string) => { + if (!accessToken) return; + try { + await routingGroupDeleteCall(accessToken, groupId); + setGroups((prev) => prev.filter((g) => g.routing_group_id !== groupId)); + } catch (err) { + console.error("Failed to delete routing group:", err); + } + }; const handleEdit = (group: Record) => { setEditTarget(group); setShowBuilder(true); - setSelectedGroup(null); - }; - - const handleTest = (group: Record) => { - setSelectedGroup(group); }; const handleClose = () => { @@ -58,16 +146,9 @@ export default function RoutingGroupsView({ } return ( -
-
-
- - Routing Groups - - - Configure named routing pipelines with fallback strategies - -
+
+
+

Routing Groups

- +
+ {/* Search and Filter Controls */} +
+
+
+
+
+ setNameSearch(e.target.value)} + /> + + + +
- {selectedGroup && accessToken && ( - <> - -
- - Testing:{" "} - - {selectedGroup.routing_group_name as string} - - - + + + +
+
+ + {showFilters && ( +
+
+