diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 0b2f467e8f8..fc7c547293b 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -37,6 +37,7 @@ import UIThemeSettings from "@/components/ui_theme_settings"; import Usage from "@/components/usage"; import UserDashboard from "@/components/user_dashboard"; import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage"; +import RoutingGroupsView from "@/components/routing_groups/RoutingGroupsView"; import { ProjectsPage } from "@/components/Projects/ProjectsPage"; import VectorStoreManagement from "@/components/vector_store_management"; import ToolPolicies from "@/components/ToolPolicies"; @@ -430,6 +431,12 @@ function CreateKeyPageContent() { addKey={addKey} createClicked={createClicked} /> + ) : page == "routing-groups" ? ( + ) : page == "models" ? ( , roles: rolesWithWriteAccess, }, + { + key: "routing-groups", + page: "routing-groups", + label: "Routing Groups", + icon: , + roles: rolesWithWriteAccess, + }, { key: "agents", page: "agents", diff --git a/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx b/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx new file mode 100644 index 00000000000..d72b971f33c --- /dev/null +++ b/ui/litellm-dashboard/src/components/routing_groups/DeploymentSelector.tsx @@ -0,0 +1,150 @@ +"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} +
+ )} + - + Back to Routing Groups + +

Routing Group Builder

+

+ Configure a named model group with a routing strategy +

+
- - - + {/* Section 1: Routing Strategy */} +
+
+ 1 · ROUTING STRATEGY +
+
+ {STRATEGIES.map((s) => { + const isActive = strategy === s.value; + return ( + + ); + })} +
+
- - 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} - /> - - + {/* Section 2: Deployments */} +
+
+
+ 2 · DEPLOYMENTS
+ +
- {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 }} + {/* Add selector */} + {showAddSelector && ( +
+
+
+ setSelectedDeployment(dep)} /> - )} +
+
- ))} +
+ )} + + {/* Pipeline visualization */} + {deployments.length > 0 && ( +
+ {deployments.map((dep, idx) => { + const color = getProviderColor(dep.provider); + return ( + + {idx > 0 && ( + → + )} + + {idx + 1} + {dep.provider !== "-" ? dep.provider.split(" ")[0] : dep.model_name} + + + ); + })} +
+ )} + + {/* Deployment cards */} + {deployments.length === 0 && !showAddSelector && ( +
+ No deployments yet. Click + Add to get started. +
+ )} + +
+ {deployments.map((dep, idx) => { + const color = getProviderColor(dep.provider); + return ( +
+ toggleEnabled(idx)} + /> + + {idx + 1} + + {/* Provider icon */} +
+ {getProviderInitial(dep.provider)} +
+ {/* Name */} +
+ + {dep.provider} + + + / {dep.model_name} + +
+ {/* Weight (only for weighted) */} + {strategy === "weighted" && ( + updateWeight(idx, val)} + addonAfter="%" + style={{ width: 90 }} + /> + )} + {/* Reorder */} +
+ + +
+ {/* Delete */} + +
+ ); + })} +
+
+ + {/* Section 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" + />
- - - - - - - - - - + {/* Code snippet */} +
+
{codeSnippet}
+
+
-
+
- - + {isEditing ? "Update Routing Group" : "Save Routing Group"} + +
+
); } diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx index 3d620b5d340..f82d770c4e0 100644 --- a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx +++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsView.tsx @@ -18,14 +18,15 @@ export default function RoutingGroupsView({ userRole, userId, }: RoutingGroupsViewProps) { - const [createModalVisible, setCreateModalVisible] = useState(false); + const [showBuilder, setShowBuilder] = useState(false); const [editTarget, setEditTarget] = useState | null>(null); const [refreshKey, setRefreshKey] = useState(0); const [selectedGroup, setSelectedGroup] = useState | null>(null); const handleEdit = (group: Record) => { setEditTarget(group); - setCreateModalVisible(true); + setShowBuilder(true); + setSelectedGroup(null); }; const handleTest = (group: Record) => { @@ -33,16 +34,29 @@ export default function RoutingGroupsView({ }; const handleClose = () => { - setCreateModalVisible(false); + setShowBuilder(false); setEditTarget(null); }; const handleSuccess = () => { - setCreateModalVisible(false); + setShowBuilder(false); setEditTarget(null); setRefreshKey((k) => k + 1); }; + if (showBuilder) { + return ( +
+ +
+ ); + } + return (
@@ -57,7 +71,7 @@ export default function RoutingGroupsView({ @@ -84,7 +98,6 @@ export default function RoutingGroupsView({ size="small" icon={} onClick={() => setSelectedGroup(null)} - style={{ color: "#64748b", borderColor: "#334155" }} > Close @@ -97,16 +110,6 @@ export default function RoutingGroupsView({ /> )} - - {createModalVisible && ( - - )}
); }