-
-
- {name.charAt(0).toUpperCase()}
+type NodeStatus = "idle" | "failing" | "sim-fail" | "sim-success" | "sim-skipped";
+
+const FailDot: React.FC = () => (
+
+);
+
+const ModelNodeCard: React.FC<{
+ label: string; name: string; subName: string;
+ status?: NodeStatus; onClick?: () => void;
+}> = ({ label, name, subName, status = "idle", onClick }) => {
+ const isFailing = status === "failing" || status === "sim-fail";
+ const isSuccess = status === "sim-success";
+ const isSkipped = status === "sim-skipped";
+
+ let borderColor = "#e5e7eb";
+ let bg = "#fff";
+ if (isSuccess) { borderColor = "#22c55e"; bg = "#f0fdf4"; }
+ else if (status === "sim-fail") { borderColor = "#ef4444"; bg = "#fef2f2"; }
+ else if (isSkipped) { bg = "#f9fafb"; }
+
+ return (
+
+
+ {isFailing ? : }
+
+ {onClick && (
+
+ {isFailing ? "click: unfail" : "click: fail"}
-
-
{name}
-
{subName}
+ )}
+
{label}
+
+
+
+ {name.charAt(0).toUpperCase()}
+
+
+ {isSuccess && (
+
+ )}
+ {status === "sim-fail" && (
+
+
+ Failed — trying next
+
+ )}
-
-);
+ );
+};
-const FailoverFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments }) => (
-
-
- {deployments.map((dep, i) => {
- const label = i === 0 ? "PRIMARY" : `FALLBACK-${i}`;
- const shortName = dep.model_name.split("/").pop() || dep.model_name;
- return (
-
- {i === 0 ? : }
-
-
- );
- })}
-
-
-
-);
+const FailoverFlow: React.FC<{
+ deployments: Deployment[];
+ failingIndices?: Set
;
+ simTrace?: SimTrace | null;
+ onToggleFail?: (index: number) => void;
+}> = ({ deployments, failingIndices, simTrace, onToggleFail }) => {
+ const getStatus = (i: number): NodeStatus => {
+ if (simTrace) {
+ const step = simTrace.steps.find((s) => s.index === i);
+ if (step) return step.status === "success" ? "sim-success" : "sim-fail";
+ return "sim-skipped";
+ }
+ if (failingIndices?.has(i)) return "failing";
+ return "idle";
+ };
-const WeightedFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments }) => {
+ return (
+
+
+ {deployments.map((dep, i) => {
+ const label = i === 0 ? "PRIMARY" : `FALLBACK-${i}`;
+ const shortName = dep.model_name.split("/").pop() || dep.model_name;
+ const nodeStatus = getStatus(i);
+ return (
+
+ {i === 0 ? : }
+ onToggleFail(i) : undefined}
+ />
+
+ );
+ })}
+
+
+
+ );
+};
+
+const WeightedFlow: React.FC<{
+ deployments: Deployment[];
+ failingIndices?: Set;
+ simTrace?: SimTrace | null;
+ onToggleFail?: (index: number) => void;
+}> = ({ deployments, failingIndices, simTrace, onToggleFail }) => {
const totalWeight = deployments.reduce((s, d) => s + (d.weight ?? 1), 0);
return (
@@ -136,24 +222,43 @@ const WeightedFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments })
const pct = totalWeight > 0 ? Math.round(((dep.weight ?? 1) / totalWeight) * 100) : 0;
const shortName = dep.model_name.split("/").pop() || dep.model_name;
const color = DEPLOY_COLORS[i % DEPLOY_COLORS.length];
+ const isFailing = failingIndices?.has(i);
+ const simStep = simTrace?.steps.find((s) => s.index === i);
+ const isSimSuccess = simStep?.status === "success";
+ const isSimFail = simStep?.status === "fail";
+ const isSkipped = simTrace && !simStep;
+
+ let rowBg = "#f9fafb";
+ let rowBorder = "1px solid #f3f4f6";
+ if (isSimSuccess) { rowBg = "#f0fdf4"; rowBorder = "1.5px solid #22c55e"; }
+ else if (isSimFail) { rowBg = "#fef2f2"; rowBorder = "1.5px solid #fca5a5"; }
+
return (
onToggleFail(i) : undefined}
+ style={{
+ padding: "8px 12px", backgroundColor: rowBg, borderRadius: 8, border: rowBorder,
+ cursor: onToggleFail ? "pointer" : "default", opacity: isSkipped ? 0.4 : 1, transition: "all 0.2s",
+ }}
>
{shortName.charAt(0).toUpperCase()}
-
{shortName}
+
+ {shortName}
+ {isSimSuccess && ✓ selected}
+ {isSimFail && ✗ failed}
+
{dep.model_name}
{pct}%
-
+ {isFailing || isSimFail ?
:
}
);
@@ -230,7 +335,10 @@ const ComplexityFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments }
);
-const SmartRoutingFlow: React.FC<{ deployments: Deployment[]; strategy: string }> = ({ deployments, strategy }) => {
+const SmartRoutingFlow: React.FC<{
+ deployments: Deployment[]; strategy: string;
+ failingIndices?: Set
; simTrace?: SimTrace | null; onToggleFail?: (index: number) => void;
+}> = ({ deployments, strategy, failingIndices, simTrace, onToggleFail }) => {
const meta = STRATEGY_META[strategy] ?? STRATEGY_META["simple-shuffle"];
return (
@@ -243,25 +351,48 @@ const SmartRoutingFlow: React.FC<{ deployments: Deployment[]; strategy: string }
{deployments.map((dep, i) => {
const shortName = dep.model_name.split("/").pop() || dep.model_name;
+ const isFailing = failingIndices?.has(i);
+ const simStep = simTrace?.steps.find((s) => s.index === i);
+ const isSimSuccess = simStep?.status === "success";
+ const isSkipped = simTrace && !simStep;
+
+ let rowBg = "#f9fafb";
+ let rowBorder = "1px solid #f3f4f6";
+ if (isSimSuccess) { rowBg = "#f0fdf4"; rowBorder = "1.5px solid #22c55e"; }
+
return (
-
+
onToggleFail(i) : undefined}
+ style={{
+ padding: "8px 12px", backgroundColor: rowBg, borderRadius: 8, border: rowBorder,
+ cursor: onToggleFail ? "pointer" : "default", opacity: isSkipped ? 0.4 : 1, transition: "all 0.2s",
+ }}
+ >
{shortName.charAt(0).toUpperCase()}
-
{shortName}
+
+ {shortName}
+ {isSimSuccess && ✓ selected}
+
{dep.model_name}
-
+ {isFailing ?
:
}
);
})}
-
+
);
};
@@ -271,26 +402,27 @@ const EmptyFlow: React.FC = () => (
- Add deployments to see the flow
+ Add models to see the flow
);
-export default function RoutingFlowVisualization({ strategy, deployments }: RoutingFlowVisualizationProps) {
+export default function RoutingFlowVisualization({ strategy, deployments, failingIndices, simTrace, onToggleFail }: RoutingFlowVisualizationProps) {
if (strategy === "complexity-router") return
;
if (deployments.length === 0) return
;
+ const simProps = { failingIndices, simTrace, onToggleFail };
switch (strategy) {
- case "priority-failover": return
;
- case "weighted": return
;
+ case "priority-failover": return
;
+ case "weighted": return
;
case "simple-shuffle":
case "latency-based-routing":
case "usage-based-routing-v2":
case "cost-based-routing":
case "least-busy":
- return
;
- default: return
;
+ return
;
+ default: return
;
}
}
diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx
index 94087bd36de..8ca7a8d536c 100644
--- a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx
+++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupBuilder.tsx
@@ -21,6 +21,7 @@ import { routingGroupCreateCall, routingGroupUpdateCall } from "@/components/net
import ModelSelector from "@/components/common_components/ModelSelector";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import RoutingFlowVisualization, { DEPLOY_COLORS } from "./RoutingFlowVisualization";
+import type { SimTrace } from "./RoutingFlowVisualization";
interface Deployment {
model_id: string;
@@ -112,6 +113,8 @@ export default function RoutingGroupBuilder({
const [logicalName, setLogicalName] = useState("");
const [submitting, setSubmitting] = useState(false);
const [tierModels, setTierModels] = useState
>({ SIMPLE: "", MEDIUM: "", COMPLEX: "", REASONING: "" });
+ const [failingIndices, setFailingIndices] = useState>(new Set());
+ const [simTrace, setSimTrace] = useState(null);
const dragItem = useRef(null);
const dragOverItem = useRef(null);
@@ -153,6 +156,64 @@ export default function RoutingGroupBuilder({
setDeployments(deps);
};
+ const toggleFail = (index: number) => {
+ setSimTrace(null);
+ setFailingIndices((prev) => {
+ const next = new Set(prev);
+ if (next.has(index)) next.delete(index); else next.add(index);
+ return next;
+ });
+ };
+
+ const runSimulation = () => {
+ if (deployments.length === 0) return;
+ const steps: SimTrace["steps"] = [];
+ let selectedIndex: number | null = null;
+
+ if (strategy === "priority-failover") {
+ for (let i = 0; i < deployments.length; i++) {
+ if (failingIndices.has(i)) {
+ steps.push({ index: i, status: "fail" });
+ } else {
+ steps.push({ index: i, status: "success" });
+ selectedIndex = i;
+ break;
+ }
+ }
+ } else if (strategy === "weighted") {
+ const available = deployments.map((d, i) => ({ i, w: d.weight ?? 1 })).filter((x) => !failingIndices.has(x.i));
+ if (available.length > 0) {
+ const totalW = available.reduce((s, x) => s + x.w, 0);
+ let r = Math.random() * totalW;
+ for (const x of available) {
+ r -= x.w;
+ if (r <= 0) { selectedIndex = x.i; break; }
+ }
+ if (selectedIndex === null) selectedIndex = available[available.length - 1].i;
+ }
+ for (let i = 0; i < deployments.length; i++) {
+ if (failingIndices.has(i)) steps.push({ index: i, status: "fail" });
+ else if (i === selectedIndex) steps.push({ index: i, status: "success" });
+ }
+ } else {
+ const available = deployments.map((_, i) => i).filter((i) => !failingIndices.has(i));
+ if (available.length > 0) {
+ selectedIndex = available[Math.floor(Math.random() * available.length)];
+ }
+ for (let i = 0; i < deployments.length; i++) {
+ if (failingIndices.has(i)) steps.push({ index: i, status: "fail" });
+ else if (i === selectedIndex) steps.push({ index: i, status: "success" });
+ }
+ }
+
+ setSimTrace({ steps, selectedIndex });
+ };
+
+ const resetSimulation = () => {
+ setSimTrace(null);
+ setFailingIndices(new Set());
+ };
+
const handleStrategyChange = (value: string) => {
setStrategy(value);
setDeployments((prev) => {
@@ -261,6 +322,9 @@ export default function RoutingGroupBuilder({
routing_strategy: strategy,
deployments,
};
+ if (isComplexityRouter) {
+ payload.settings = { tiers: tierModels };
+ }
if (isEditing && editTarget?.routing_group_id) {
await routingGroupUpdateCall(token, editTarget.routing_group_id as string, payload);
message.success("Routing group updated");
@@ -343,7 +407,7 @@ export default function RoutingGroupBuilder({
) : (
<>
-
2 · Deployments
+
2 · Model Group
{/* Flow canvas */}
-
+
+ {strategy !== "complexity-router" && deployments.length > 0 && (
+
+ Click any deployment to toggle failure · then press Send Request to simulate
+
+ )}
-
+
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
index 533e51d31bb..8eb2ad67763 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
@@ -132,6 +132,9 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
+ {/* Routing Info */}
+
+
{/* Metrics */}
@@ -499,6 +502,193 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
);
}
+const STRATEGY_LABELS: Record = {
+ "simple-shuffle": "Random",
+ "latency-based-routing": "Lowest Latency",
+ "usage-based-routing-v2": "Usage-Based",
+ "cost-based-routing": "Lowest Cost",
+ "least-busy": "Least Busy",
+ "priority-failover": "Ordered Fallback",
+ "weighted-round-robin": "Weighted Round Robin",
+ "complexity-router": "Complexity Router",
+};
+
+function RoutingSection({
+ metadata,
+ modelGroup,
+ selectedModel,
+ selectedModelId,
+}: {
+ metadata: Record;
+ modelGroup?: string;
+ selectedModel: string;
+ selectedModelId: string;
+}) {
+ const modelGroupSize = metadata?.model_group_size;
+ const routingStrategy = metadata?.routing_strategy;
+ const candidates: Array<{ model_id?: string; model_name?: string; litellm_model?: string; priority?: number }> =
+ metadata?.model_group_candidates || [];
+
+ if (!modelGroup) return null;
+
+ const isRoutingGroup = modelGroup !== selectedModel;
+ const strategyLabel = (routingStrategy && STRATEGY_LABELS[routingStrategy]) || routingStrategy;
+ const isOrdered = routingStrategy === "priority-failover";
+ const sortedCandidates = isOrdered
+ ? [...candidates].sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999))
+ : candidates;
+
+ return (
+
+
+ Routing
+ {isRoutingGroup && (
+ Routing Group
+ )}
+
+ }
+ size="small"
+ bordered={false}
+ style={{ marginBottom: 0 }}
+ >
+
+
+ {modelGroup}
+
+
+ {selectedModel}
+ {selectedModelId && (
+
+ ({selectedModelId.slice(0, 8)}…)
+
+ )}
+
+ {routingStrategy && (
+
+ {strategyLabel}
+
+ )}
+ {modelGroupSize != null && (
+
+ {modelGroupSize} deployment{modelGroupSize !== 1 ? "s" : ""}
+
+ )}
+
+
+ {/* Candidate list */}
+ {sortedCandidates.length > 0 && (
+
+
+ {isOrdered ? "Fallback Order" : "Candidates Considered"}
+
+
+ {sortedCandidates.map((c, idx) => {
+ const isSelected = c.model_id === selectedModelId;
+ const label = c.litellm_model || c.model_name || c.model_id || "unknown";
+ return (
+
+
+ {isOrdered && (
+
+ {idx + 1}
+
+ )}
+ {!isOrdered && (
+
+ )}
+
+
+ {label}
+
+ {c.model_id && (
+
+ {c.model_id.slice(0, 8)}…
+
+ )}
+
+ {isSelected && (
+
selected
+ )}
+ {isOrdered && !isSelected && idx === 0 && (
+
primary
+ )}
+
+ {/* Connector line between items */}
+ {isOrdered && idx < sortedCandidates.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ {/* Explanation */}
+ {modelGroupSize != null && modelGroupSize > 1 && (
+
+
+ {routingStrategy === "simple-shuffle" && (
+ <>Randomly selected from {modelGroupSize} available deployments.>
+ )}
+ {routingStrategy === "latency-based-routing" && (
+ <>Picked the lowest-latency deployment from {modelGroupSize} candidates.>
+ )}
+ {routingStrategy === "usage-based-routing-v2" && (
+ <>Picked the deployment with the most available TPM/RPM capacity.>
+ )}
+ {routingStrategy === "cost-based-routing" && (
+ <>Picked the lowest-cost deployment from {modelGroupSize} candidates.>
+ )}
+ {routingStrategy === "least-busy" && (
+ <>Picked the deployment with the fewest active requests.>
+ )}
+ {routingStrategy === "priority-failover" && (
+ <>Tried deployments in priority order. {
+ sortedCandidates.length > 0 && sortedCandidates[0]?.model_id === selectedModelId
+ ? "Primary deployment was healthy."
+ : `Primary was unavailable — fell back to priority ${(sortedCandidates.findIndex(c => c.model_id === selectedModelId) + 1) || "?"}.`
+ }>
+ )}
+ {!routingStrategy && (
+ <>Router selected {selectedModel} from {modelGroupSize} deployments.>
+ )}
+ {routingStrategy && !["simple-shuffle", "latency-based-routing", "usage-based-routing-v2", "cost-based-routing", "least-busy", "priority-failover"].includes(routingStrategy) && (
+ <>Strategy: {strategyLabel}. Selected from {modelGroupSize} candidates.>
+ )}
+
+
+ )}
+
+