From ec6e10e04363881c4c1700bf6b260711e9fd7ab6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 28 Feb 2026 18:00:37 -0800 Subject: [PATCH] feat(ui): add RoutingGroupBuilder component --- .../routing_groups/RoutingGroupBuilder.tsx | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx new file mode 100644 index 00000000000..df2e5ee76c3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx @@ -0,0 +1,312 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + Button, + Collapse, + Form, + Input, + InputNumber, + Modal, + Radio, + Space, + Typography, +} from "antd"; +import { CloseOutlined, PlusOutlined } from "@ant-design/icons"; +import { routingGroupCreateCall, routingGroupUpdateCall } from "@/components/networking"; + +interface Deployment { + model_name: string; + litellm_provider: string; + weight?: number; + priority?: number; +} + +interface RoutingGroupBuilderProps { + accessToken: string | null; + visible: boolean; + editTarget: Record | null; + onClose: () => void; + onSuccess: () => void; +} + +const ROUTING_STRATEGIES = [ + { value: "priority-failover", label: "Priority Failover: ordered, first success wins" }, + { value: "weighted", label: "Weighted: split traffic by percentage" }, + { value: "cost-based-routing", label: "Cost-Based: route to cheapest" }, + { value: "latency-based-routing", label: "Latency-Based: route to fastest" }, + { value: "least-busy", label: "Least Busy: fewest in-flight requests" }, + { value: "usage-based-routing-v2", label: "Usage-Based: TPM/RPM aware" }, + { value: "simple-shuffle", label: "Round Robin: even distribution" }, +]; + +export default function RoutingGroupBuilder({ + accessToken, + visible, + editTarget, + onClose, + onSuccess, +}: RoutingGroupBuilderProps) { + const [form] = Form.useForm(); + const [deployments, setDeployments] = useState([]); + const [newModelName, setNewModelName] = useState(""); + const [newProvider, setNewProvider] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [strategy, setStrategy] = useState("priority-failover"); + + const isEditing = editTarget !== null; + + useEffect(() => { + if (editTarget) { + form.setFieldsValue({ + routing_group_name: editTarget.routing_group_name, + description: editTarget.description, + routing_strategy: editTarget.routing_strategy ?? "priority-failover", + max_retries: editTarget.max_retries, + cooldown_time: editTarget.cooldown_time, + }); + setStrategy((editTarget.routing_strategy as string) ?? "priority-failover"); + if (Array.isArray(editTarget.deployments)) { + setDeployments(editTarget.deployments as Deployment[]); + } + } else { + form.resetFields(); + setDeployments([]); + setStrategy("priority-failover"); + } + }, [editTarget, form]); + + const addDeployment = () => { + const trimmedModel = newModelName.trim(); + if (!trimmedModel) return; + const newDeployment: Deployment = { + model_name: trimmedModel, + litellm_provider: newProvider.trim(), + weight: strategy === "weighted" ? 1 : undefined, + priority: strategy === "priority-failover" ? deployments.length + 1 : undefined, + }; + setDeployments((prev) => [...prev, newDeployment]); + setNewModelName(""); + setNewProvider(""); + }; + + const removeDeployment = (index: number) => { + setDeployments((prev) => { + const updated = prev.filter((_, i) => i !== index); + if (strategy === "priority-failover") { + return updated.map((d, i) => ({ ...d, priority: i + 1 })); + } + return updated; + }); + }; + + const updateDeploymentWeight = (index: number, weight: number | null) => { + setDeployments((prev) => + prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d)) + ); + }; + + const handleStrategyChange = (value: string) => { + setStrategy(value); + setDeployments((prev) => + prev.map((d, i) => ({ + ...d, + weight: value === "weighted" ? (d.weight ?? 1) : undefined, + priority: value === "priority-failover" ? i + 1 : undefined, + })) + ); + }; + + const handleSubmit = async () => { + if (!accessToken) return; + try { + const values = await form.validateFields(); + setSubmitting(true); + + const payload: Record = { + routing_group_name: values.routing_group_name, + description: values.description, + routing_strategy: values.routing_strategy, + deployments, + max_retries: values.max_retries, + cooldown_time: values.cooldown_time, + }; + + if (isEditing && editTarget?.routing_group_id) { + await routingGroupUpdateCall( + accessToken, + editTarget.routing_group_id as string, + payload + ); + } else { + await routingGroupCreateCall(accessToken, payload); + } + + onSuccess(); + } catch (err) { + console.error("Failed to save routing group:", err); + } finally { + setSubmitting(false); + } + }; + + return ( + + Cancel + , + , + ]} + > +
+ + + + + + + + + + handleStrategyChange(e.target.value)} + > + + {ROUTING_STRATEGIES.map((s) => ( + + {s.label} + + ))} + + + + +
+ Deployments +
+ + setNewModelName(e.target.value)} + style={{ width: 200 }} + onPressEnter={addDeployment} + /> + setNewProvider(e.target.value)} + style={{ width: 160 }} + onPressEnter={addDeployment} + /> + + +
+ + {deployments.length === 0 && ( + + No deployments added yet. + + )} + + {deployments.map((dep, idx) => ( +
+ {strategy === "priority-failover" && ( + + #{dep.priority} + + )} + + {dep.model_name} + {dep.litellm_provider ? ` (${dep.litellm_provider})` : ""} + + {strategy === "weighted" && ( + updateDeploymentWeight(idx, val)} + addonAfter="%" + style={{ width: 100 }} + /> + )} +
+ ))} +
+ + + + + + + + + + + + +
+ + Access is managed via Teams.{" "} + + Manage Teams + + +
+
+
+ ); +}