This commit is contained in:
Ishaan Jaffer 2026-03-02 20:13:04 -08:00
parent f39adbc50b
commit 4611f8f889
3 changed files with 381 additions and 341 deletions

View file

@ -1097,6 +1097,26 @@ model LiteLLM_AccessGroupTable {
updated_at DateTime @default(now()) @updatedAt
updated_by String?
}
// Routing Groups table for storing named routing pipelines
model LiteLLM_RoutingGroupTable {
routing_group_id String @id @default(uuid())
routing_group_name String @unique
description String?
routing_strategy String
deployments Json
fallback_config Json? @default("{}")
retry_config Json? @default("{}")
cooldown_config Json? @default("{}")
settings Json? @default("{}")
assigned_team_ids String[] @default([])
assigned_key_ids String[] @default([])
is_active Boolean @default(true)
created_at DateTime @default(now())
created_by String
updated_at DateTime @default(now()) @updatedAt
updated_by String
}
// Claude Code Plugin Marketplace table
model LiteLLM_ClaudeCodePluginTable {
id String @id @default(uuid())

View file

@ -15,7 +15,7 @@ interface RoutingFlowVisualizationProps {
deployments: Deployment[];
}
const DEPLOY_COLORS = ["#10b981", "#f59e0b", "#3b82f6", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
const DEPLOY_COLORS = ["#3b82f6", "#6366f1", "#0ea5e9"];
const VerticalLine: React.FC<{ height?: number }> = ({ height = 28 }) => (
<div className="flex justify-center" style={{ height }}>
@ -27,63 +27,22 @@ const StatusDot: React.FC = () => (
<div style={{ width: 8, height: 8, borderRadius: "50%", backgroundColor: "#22c55e", flexShrink: 0 }} />
);
const KillTag: React.FC = () => (
<span style={{ fontSize: 10, fontWeight: 600, color: "#ef4444", backgroundColor: "#fef2f2", padding: "2px 8px", borderRadius: 4, flexShrink: 0 }}>
kill
</span>
);
const FailConnector: React.FC = () => (
<div className="flex flex-col items-center" style={{ margin: "4px 0" }}>
<VerticalLine height={12} />
<div className="flex items-center gap-1.5" style={{ padding: "3px 10px", backgroundColor: "#fef2f2", borderRadius: 12 }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M15 9l-6 6M9 9l6 6" />
<circle cx="12" cy="12" r="10" /><path d="M15 9l-6 6M9 9l6 6" />
</svg>
<span style={{ fontSize: 10, fontWeight: 700, color: "#ef4444", letterSpacing: "0.04em" }}>
FAIL → TRY NEXT
</span>
<span style={{ fontSize: 10, fontWeight: 700, color: "#ef4444", letterSpacing: "0.04em" }}>FAIL → TRY NEXT</span>
</div>
<VerticalLine height={12} />
</div>
);
const ProviderIcon: React.FC<{ color: string; name: string }> = ({ color, name }) => (
<div
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: color + "18",
border: `1.5px solid ${color}40`,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
fontSize: 13,
fontWeight: 700,
color: color,
}}
>
{name.charAt(0).toUpperCase()}
</div>
);
const TriggerCard: React.FC<{ subtitle: string }> = ({ subtitle }) => (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 12,
padding: "16px 20px",
backgroundColor: "#fff",
width: "100%",
maxWidth: 340,
}}
>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>
TRIGGER
</div>
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "16px 20px", backgroundColor: "#fff", width: "100%", maxWidth: 340 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>TRIGGER</div>
<div className="flex items-center gap-2">
<div style={{ width: 28, height: 28, borderRadius: "50%", backgroundColor: "#f3f4f6", display: "flex", alignItems: "center", justifyContent: "center" }}>
<svg width="11" height="11" viewBox="0 0 24 24" fill="#6b7280" stroke="none"><polygon points="6,3 20,12 6,21" /></svg>
@ -97,19 +56,8 @@ const TriggerCard: React.FC<{ subtitle: string }> = ({ subtitle }) => (
);
const OutputCard: React.FC<{ subtitle: string }> = ({ subtitle }) => (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 12,
padding: "16px 20px",
backgroundColor: "#fff",
width: "100%",
maxWidth: 340,
}}
>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>
OUTPUT
</div>
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "16px 20px", backgroundColor: "#fff", width: "100%", maxWidth: 340 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>OUTPUT</div>
<div className="flex items-center gap-2">
<div style={{ width: 28, height: 28, borderRadius: "50%", backgroundColor: "#f0fdf4", display: "flex", alignItems: "center", justifyContent: "center" }}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@ -124,36 +72,20 @@ const OutputCard: React.FC<{ subtitle: string }> = ({ subtitle }) => (
</div>
);
const ModelCard: React.FC<{
label: string;
name: string;
subName: string;
color: string;
}> = ({ label, name, subName, color }) => (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 12,
padding: "14px 18px",
backgroundColor: "#fff",
width: "100%",
maxWidth: 340,
position: "relative",
}}
>
const ModelNodeCard: React.FC<{ label: string; name: string; subName: string }> = ({ label, name, subName }) => (
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "14px 18px", backgroundColor: "#fff", width: "100%", maxWidth: 340, position: "relative" }}>
<div style={{ position: "absolute", top: 12, right: 14 }}><StatusDot /></div>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 6 }}>
{label}
</div>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 6 }}>{label}</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<ProviderIcon color={color} name={name} />
<div style={{ width: 32, height: 32, borderRadius: "50%", backgroundColor: "#eff6ff", border: "1.5px solid #bfdbfe", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 13, fontWeight: 700, color: "#3b82f6" }}>
{name.charAt(0).toUpperCase()}
</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#111827" }}>{name}</div>
<div style={{ fontSize: 12, color: "#9ca3af" }}>{subName}</div>
</div>
</div>
<KillTag />
</div>
</div>
);
@ -165,14 +97,9 @@ const FailoverFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments })
const label = i === 0 ? "PRIMARY" : `FALLBACK-${i}`;
const shortName = dep.model_name.split("/").pop() || dep.model_name;
return (
<React.Fragment key={dep.model_id + i}>
<React.Fragment key={dep.model_id + "-" + i}>
{i === 0 ? <VerticalLine /> : <FailConnector />}
<ModelCard
label={label}
name={shortName}
subName={dep.model_name}
color={DEPLOY_COLORS[i % DEPLOY_COLORS.length]}
/>
<ModelNodeCard label={label} name={shortName} subName={dep.model_name} />
</React.Fragment>
);
})}
@ -189,123 +116,139 @@ const WeightedFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments })
<TriggerCard subtitle="→ weighted distribution" />
<VerticalLine />
{/* Load balancer split node */}
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 12,
padding: "14px 18px",
backgroundColor: "#fff",
width: "100%",
maxWidth: 340,
}}
>
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "14px 18px", backgroundColor: "#fff", width: "100%", maxWidth: 400 }}>
<div className="flex items-center gap-2" style={{ marginBottom: 10 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b7280" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5" />
</svg>
<span style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em" }}>
LOAD BALANCER
</span>
<span style={{ fontSize: 10, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em" }}>LOAD BALANCER</span>
</div>
{/* Weight bar */}
<div style={{ width: "100%", height: 8, borderRadius: 4, overflow: "hidden", display: "flex", backgroundColor: "#f3f4f6", marginBottom: 4 }}>
<div style={{ width: "100%", height: 6, borderRadius: 3, overflow: "hidden", display: "flex", backgroundColor: "#e5e7eb", marginBottom: 12 }}>
{deployments.map((dep, i) => {
const pct = totalWeight > 0 ? ((dep.weight ?? 1) / totalWeight) * 100 : 0;
return (
<div key={dep.model_id + i} style={{ width: `${pct}%`, height: "100%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />
);
return <div key={dep.model_id + "-bar-" + i} style={{ width: `${pct}%`, height: "100%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />;
})}
</div>
</div>
{/* Fan-out lines from center */}
<div style={{ position: "relative", width: "100%", maxWidth: 500 }}>
{/* Vertical stem */}
<div className="flex justify-center"><VerticalLine height={16} /></div>
{/* Horizontal bar */}
<div style={{ position: "relative", marginLeft: "10%", marginRight: "10%", height: 2, backgroundColor: "#e5e7eb" }} />
{/* Branch lines + cards */}
<div
style={{
display: "flex",
gap: 12,
width: "100%",
paddingLeft: "2%",
paddingRight: "2%",
}}
>
<div className="flex flex-col gap-2">
{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;
const color = DEPLOY_COLORS[i % DEPLOY_COLORS.length];
return (
<div key={dep.model_id + i} className="flex flex-col items-center flex-1" style={{ minWidth: 0 }}>
<VerticalLine height={16} />
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 10,
padding: "10px 8px",
backgroundColor: "#fff",
width: "100%",
textAlign: "center",
position: "relative",
}}
>
<div style={{ position: "absolute", top: 6, right: 6 }}><StatusDot /></div>
<div style={{ fontSize: 10, fontWeight: 700, color: DEPLOY_COLORS[i % DEPLOY_COLORS.length], marginBottom: 2 }}>
{pct}%
<div
key={dep.model_id + "-row-" + i}
className="flex items-center justify-between"
style={{ padding: "8px 12px", backgroundColor: "#f9fafb", borderRadius: 8, border: "1px solid #f3f4f6" }}
>
<div className="flex items-center gap-2.5">
<div style={{ width: 28, height: 28, borderRadius: "50%", backgroundColor: color + "18", border: `1.5px solid ${color}44`, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 11, fontWeight: 700, color }}>
{shortName.charAt(0).toUpperCase()}
</div>
<ProviderIcon color={DEPLOY_COLORS[i % DEPLOY_COLORS.length]} name={shortName} />
<div style={{ fontSize: 11, fontWeight: 600, color: "#111827", marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={dep.model_name}>
{shortName}
</div>
<div style={{ fontSize: 10, color: "#9ca3af", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{dep.model_name}
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: "#111827" }}>{shortName}</div>
<div style={{ fontSize: 10, color: "#9ca3af" }}>{dep.model_name}</div>
</div>
</div>
<VerticalLine height={16} />
<div className="flex items-center gap-2">
<span style={{ fontSize: 13, fontWeight: 700, color }}>{pct}%</span>
<div style={{ width: 8, height: 8, borderRadius: "50%", backgroundColor: "#22c55e", flexShrink: 0 }} />
</div>
</div>
);
})}
</div>
{/* Fan-in horizontal bar */}
<div style={{ position: "relative", marginLeft: "10%", marginRight: "10%", height: 2, backgroundColor: "#e5e7eb" }} />
<div className="flex justify-center"><VerticalLine height={16} /></div>
</div>
<VerticalLine />
<OutputCard subtitle="weighted distribution" />
</div>
);
};
const STRATEGY_META: Record<string, { label: string; subtitle: string; output: string; color: string }> = {
"simple-shuffle": { label: "SHUFFLE ROUTER", subtitle: "→ random selection", output: "random deployment selected", color: "#6b7280" },
"latency-based-routing": { label: "LATENCY ROUTER", subtitle: "→ latency-based", output: "fastest model wins", color: "#6366f1" },
"usage-based-routing-v2": { label: "USAGE ROUTER", subtitle: "→ usage-based", output: "least busy model selected", color: "#6366f1" },
"cost-based-routing": { label: "COST ROUTER", subtitle: "→ cost-based", output: "cheapest deployment selected", color: "#059669" },
"least-busy": { label: "LEAST BUSY ROUTER", subtitle: "→ least busy", output: "deployment with fewest requests", color: "#6366f1" },
};
const COMPLEXITY_TIER_LABELS = [
{ tier: "SIMPLE", label: "Simple", threshold: "< 0.15" },
{ tier: "MEDIUM", label: "Medium", threshold: "0.15 – 0.35" },
{ tier: "COMPLEX", label: "Complex", threshold: "0.35 – 0.60" },
{ tier: "REASONING", label: "Reasoning", threshold: "> 0.60" },
];
const ComplexityFlow: React.FC<{ deployments: Deployment[] }> = ({ deployments }) => (
<div className="flex flex-col items-center">
<TriggerCard subtitle="→ auto complexity scoring" />
<VerticalLine />
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "14px 18px", backgroundColor: "#fff", width: "100%", maxWidth: 400 }}>
<div className="flex items-center gap-2" style={{ marginBottom: 4 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b7280" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<span style={{ fontSize: 10, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.06em" }}>AUTO ROUTER</span>
</div>
<div style={{ fontSize: 10, color: "#9ca3af", marginBottom: 12 }}>Rule-based scoring &middot; no API calls &middot; &lt;1ms</div>
<div className="flex flex-col gap-2">
{COMPLEXITY_TIER_LABELS.map((t, i) => {
const dep = deployments[i];
const shortName = dep ? (dep.model_name.split("/").pop() || dep.model_name) : null;
return (
<div key={t.tier} style={{ padding: "10px 12px", backgroundColor: "#f9fafb", borderRadius: 8, border: "1px solid #f3f4f6", borderLeft: "3px solid #d1d5db" }}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span style={{ fontSize: 12, fontWeight: 700, color: "#374151" }}>{t.label}</span>
<span style={{ fontSize: 10, color: "#9ca3af" }}>score {t.threshold}</span>
</div>
</div>
{shortName ? (
<div className="flex items-center gap-2 mt-1.5">
<div style={{ width: 22, height: 22, borderRadius: "50%", backgroundColor: "#eff6ff", border: "1.5px solid #bfdbfe", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 10, fontWeight: 700, color: "#3b82f6" }}>
{shortName.charAt(0).toUpperCase()}
</div>
<span style={{ fontSize: 12, fontWeight: 600, color: "#111827" }}>{shortName}</span>
<StatusDot />
</div>
) : (
<div style={{ fontSize: 11, color: "#9ca3af", fontStyle: "italic", marginTop: 4 }}>No model assigned</div>
)}
</div>
);
})}
</div>
</div>
<VerticalLine />
<OutputCard subtitle="complexity-matched model responds" />
</div>
);
const SmartRoutingFlow: React.FC<{ deployments: Deployment[]; strategy: string }> = ({ deployments, strategy }) => {
const isLatency = strategy === "latency-based-routing";
const meta = STRATEGY_META[strategy] ?? STRATEGY_META["simple-shuffle"];
return (
<div className="flex flex-col items-center">
<TriggerCard subtitle={isLatency ? "→ latency-based" : "→ usage-based"} />
<TriggerCard subtitle={meta.subtitle} />
<VerticalLine />
<div
style={{
border: "1px solid #8b5cf6",
borderRadius: 12,
padding: "14px 18px",
backgroundColor: "#faf5ff",
width: "100%",
maxWidth: 340,
}}
>
<div style={{ fontSize: 10, fontWeight: 700, color: "#8b5cf6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 8 }}>
{isLatency ? "LATENCY ROUTER" : "USAGE ROUTER"}
<div style={{ border: "1px solid #e5e7eb", borderRadius: 12, padding: "14px 18px", backgroundColor: "#fff", width: "100%", maxWidth: 340 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: meta.color, textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 8 }}>
{meta.label}
</div>
<div className="flex flex-col gap-2">
{deployments.map((dep, i) => {
const shortName = dep.model_name.split("/").pop() || dep.model_name;
return (
<div key={dep.model_id + i} className="flex items-center justify-between" style={{ padding: "6px 10px", backgroundColor: "#fff", borderRadius: 8, border: "1px solid #e5e7eb" }}>
<div key={dep.model_id + "-" + i} className="flex items-center justify-between" style={{ padding: "8px 12px", backgroundColor: "#f9fafb", borderRadius: 8, border: "1px solid #f3f4f6" }}>
<div className="flex items-center gap-2">
<ProviderIcon color={DEPLOY_COLORS[i % DEPLOY_COLORS.length]} name={shortName} />
<div style={{ width: 28, height: 28, borderRadius: "50%", backgroundColor: "#eff6ff", border: "1.5px solid #bfdbfe", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 11, fontWeight: 700, color: "#3b82f6" }}>
{shortName.charAt(0).toUpperCase()}
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: "#111827" }}>{shortName}</div>
<div style={{ fontSize: 10, color: "#9ca3af" }}>{dep.model_name}</div>
@ -317,9 +260,8 @@ const SmartRoutingFlow: React.FC<{ deployments: Deployment[]; strategy: string }
})}
</div>
</div>
<VerticalLine />
<OutputCard subtitle={isLatency ? "fastest model wins" : "least busy model selected"} />
<OutputCard subtitle={meta.output} />
</div>
);
};
@ -337,13 +279,18 @@ const EmptyFlow: React.FC = () => (
);
export default function RoutingFlowVisualization({ strategy, deployments }: RoutingFlowVisualizationProps) {
if (strategy === "complexity-router") return <ComplexityFlow deployments={deployments} />;
if (deployments.length === 0) return <EmptyFlow />;
switch (strategy) {
case "priority-failover": return <FailoverFlow deployments={deployments} />;
case "weighted": return <WeightedFlow deployments={deployments} />;
case "simple-shuffle":
case "latency-based-routing":
case "usage-based-routing-v2": return <SmartRoutingFlow deployments={deployments} strategy={strategy} />;
default: return <FailoverFlow deployments={deployments} />;
case "usage-based-routing-v2":
case "cost-based-routing":
case "least-busy":
return <SmartRoutingFlow deployments={deployments} strategy={strategy} />;
default: return <SmartRoutingFlow deployments={deployments} strategy={strategy} />;
}
}

View file

@ -1,17 +1,21 @@
"use client";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Button, InputNumber } from "antd";
import { Button, InputNumber, message } from "antd";
import {
ArrowLeftOutlined,
CodeOutlined,
DeleteOutlined,
DollarOutlined,
ForkOutlined,
HolderOutlined,
OrderedListOutlined,
PlusOutlined,
PercentageOutlined,
SwapOutlined,
ThunderboltOutlined,
DashboardOutlined,
NodeIndexOutlined,
} from "@ant-design/icons";
import { routingGroupCreateCall, routingGroupUpdateCall } from "@/components/networking";
import ModelSelector from "@/components/common_components/ModelSelector";
@ -34,6 +38,12 @@ interface RoutingGroupBuilderProps {
}
const STRATEGIES = [
{
value: "simple-shuffle",
label: "Simple Shuffle",
desc: "Random selection across deployments",
icon: <SwapOutlined style={{ fontSize: 20 }} />,
},
{
value: "priority-failover",
label: "Ordered Fallback",
@ -58,6 +68,31 @@ const STRATEGIES = [
desc: "Route to fastest responding backend",
icon: <ThunderboltOutlined style={{ fontSize: 20 }} />,
},
{
value: "cost-based-routing",
label: "Cost-Based",
desc: "Route to cheapest deployment",
icon: <DollarOutlined style={{ fontSize: 20 }} />,
},
{
value: "least-busy",
label: "Least Busy",
desc: "Route to deployment with fewest active requests",
icon: <NodeIndexOutlined style={{ fontSize: 20 }} />,
},
{
value: "complexity-router",
label: "Auto Router",
desc: "Route by request complexity, no API calls, <1ms",
icon: <ForkOutlined style={{ fontSize: 20 }} />,
},
];
const COMPLEXITY_TIERS: { key: string; label: string; desc: string; examples: string }[] = [
{ key: "SIMPLE", label: "Simple", desc: "Basic questions, greetings, simple facts", examples: '"Hello!", "What is Python?"' },
{ key: "MEDIUM", label: "Medium", desc: "Standard queries requiring explanation", examples: '"Explain REST APIs", "Debug this error"' },
{ key: "COMPLEX", label: "Complex", desc: "Technical, multi-part deep requests", examples: '"Design a microservices arch"' },
{ key: "REASONING", label: "Reasoning", desc: "Chain-of-thought, analysis", examples: '"Think step by step..."' },
];
const INITIAL_SLOT_COUNT = 3;
@ -71,11 +106,12 @@ export default function RoutingGroupBuilder({
const { accessToken: authToken } = useAuthorized();
const effectiveToken = accessToken ?? authToken ?? "";
const [strategy, setStrategy] = useState("priority-failover");
const [strategy, setStrategy] = useState("simple-shuffle");
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [emptySlots, setEmptySlots] = useState(INITIAL_SLOT_COUNT);
const [logicalName, setLogicalName] = useState("");
const [submitting, setSubmitting] = useState(false);
const [tierModels, setTierModels] = useState<Record<string, string>>({ SIMPLE: "", MEDIUM: "", COMPLEX: "", REASONING: "" });
const dragItem = useRef<number | null>(null);
const dragOverItem = useRef<number | null>(null);
@ -105,27 +141,46 @@ export default function RoutingGroupBuilder({
[deployments]
);
const isComplexityRouter = strategy === "complexity-router";
const handleTierModelChange = (tierKey: string, model: string) => {
const updated = { ...tierModels, [tierKey]: model };
setTierModels(updated);
const deps = COMPLEXITY_TIERS
.map((t) => updated[t.key])
.filter(Boolean)
.map((name) => ({ model_id: name, model_name: name, provider: "-" }));
setDeployments(deps);
};
const handleStrategyChange = (value: string) => {
setStrategy(value);
setDeployments((prev) =>
prev.map((d, i) => ({
setDeployments((prev) => {
const equal = prev.length > 0 ? Math.max(1, Math.round(100 / prev.length)) : 1;
return prev.map((d, i) => ({
...d,
weight: value === "weighted" ? (d.weight ?? 1) : undefined,
weight: value === "weighted" ? equal : undefined,
priority: value === "priority-failover" ? i + 1 : undefined,
}))
);
}));
});
};
const handleSlotSelected = (slotIndex: number, modelGroup: string) => {
if (!modelGroup || deployments.some((d) => d.model_name === modelGroup)) return;
const count = deployments.length + 1;
const equal = Math.max(1, Math.round(100 / count));
const newDep: Deployment = {
model_id: modelGroup,
model_name: modelGroup,
provider: "-",
weight: strategy === "weighted" ? 1 : undefined,
priority: strategy === "priority-failover" ? deployments.length + 1 : undefined,
weight: strategy === "weighted" ? equal : undefined,
priority: strategy === "priority-failover" ? count : undefined,
};
setDeployments((prev) => [...prev, newDep]);
if (strategy === "weighted") {
setDeployments((prev) => [...prev.map((d) => ({ ...d, weight: equal })), newDep]);
} else {
setDeployments((prev) => [...prev, newDep]);
}
setEmptySlots((prev) => Math.max(0, prev - 1));
};
@ -151,10 +206,16 @@ export default function RoutingGroupBuilder({
});
};
const updateWeight = (index: number, weight: number | null) => {
setDeployments((prev) =>
prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d))
);
const updatePercent = (index: number, newPct: number | null) => {
const pct = Math.max(1, Math.min(99, newPct ?? 1));
setDeployments((prev) => {
const otherTotal = prev.reduce((s, d, i) => i === index ? s : s + (d.weight ?? 1), 0);
if (otherTotal === 0) return prev;
const scale = (100 - pct) / otherTotal;
return prev.map((d, i) =>
i === index ? { ...d, weight: pct } : { ...d, weight: Math.max(1, Math.round((d.weight ?? 1) * scale)) }
);
});
};
const handleDragStart = (index: number) => { dragItem.current = index; };
@ -180,7 +241,19 @@ export default function RoutingGroupBuilder({
};
const handleSubmit = async () => {
if (!accessToken || !logicalName.trim() || deployments.length === 0) return;
const token = accessToken ?? effectiveToken;
if (!token) {
message.error("No access token available");
return;
}
if (!logicalName.trim()) {
message.error("Please enter a logical model name");
return;
}
if (deployments.length === 0) {
message.error("Please add at least one deployment");
return;
}
try {
setSubmitting(true);
const payload: Record<string, unknown> = {
@ -189,13 +262,16 @@ export default function RoutingGroupBuilder({
deployments,
};
if (isEditing && editTarget?.routing_group_id) {
await routingGroupUpdateCall(accessToken, editTarget.routing_group_id as string, payload);
await routingGroupUpdateCall(token, editTarget.routing_group_id as string, payload);
message.success("Routing group updated");
} else {
await routingGroupCreateCall(accessToken, payload);
await routingGroupCreateCall(token, payload);
message.success("Routing group created");
}
onSuccess();
} catch (err) {
console.error("Failed to save routing group:", err);
message.error("Failed to save: " + (err instanceof Error ? err.message : String(err)));
} finally {
setSubmitting(false);
}
@ -207,7 +283,7 @@ export default function RoutingGroupBuilder({
<div className="w-full max-w-screen overflow-x-hidden box-border">
<div className="flex" style={{ minHeight: "100vh" }}>
{/* ── LEFT: Config Panel ── */}
<div style={{ width: 380, flexShrink: 0, borderRight: "1px solid #e5e7eb", backgroundColor: "#fff", overflowY: "auto", padding: "20px 20px 48px" }}>
<div style={{ width: 480, flexShrink: 0, borderRight: "1px solid #e5e7eb", backgroundColor: "#fff", overflowY: "auto", padding: "20px 24px 48px" }}>
<button
onClick={onClose}
className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-3 bg-transparent border-none cursor-pointer p-0"
@ -233,151 +309,170 @@ export default function RoutingGroupBuilder({
<div className="mb-1.5 text-gray-400">{s.icon}</div>
<div className="font-semibold text-gray-900 text-xs">{s.label}</div>
<div className="text-xs text-gray-500 mt-0.5 leading-tight">{s.desc}</div>
{isActive && (
<span className="inline-block mt-1.5 text-xs font-semibold text-blue-600 bg-blue-50 rounded px-1.5 py-0.5">
Active
</span>
)}
</button>
);
})}
</div>
</div>
{/* 2 · Deployments */}
{/* 2 · Deployments / Tier Config */}
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-gray-400 tracking-wide uppercase">2 &middot; Deployments</div>
<button
onClick={addEmptySlot}
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700 bg-transparent border-none cursor-pointer p-0 font-medium"
>
<PlusOutlined /> Add
</button>
</div>
{/* Pipeline breadcrumb chain */}
{deployments.length > 0 && (
<div className="flex items-center flex-wrap gap-1 mb-3">
{deployments.map((dep, i) => {
const shortName = dep.model_name.split("/").pop() || dep.model_name;
return (
<React.Fragment key={dep.model_id + i}>
{i > 0 && <span style={{ fontSize: 10, color: "#9ca3af" }}>→</span>}
<span
style={{
fontSize: 11,
fontWeight: 600,
color: DEPLOY_COLORS[i % DEPLOY_COLORS.length],
backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] + "14",
border: `1px solid ${DEPLOY_COLORS[i % DEPLOY_COLORS.length]}30`,
padding: "2px 8px",
borderRadius: 6,
display: "inline-flex",
alignItems: "center",
gap: 4,
}}
>
<span style={{ fontWeight: 700, fontSize: 10 }}>{i + 1}</span>
{shortName}
</span>
</React.Fragment>
);
})}
</div>
)}
{/* Weight distribution bar */}
{strategy === "weighted" && deployments.length > 0 && (
<div className="mb-3">
<div style={{ width: "100%", height: 6, borderRadius: 3, overflow: "hidden", display: "flex", backgroundColor: "#e5e7eb" }}>
{deployments.map((dep, i) => {
const pct = totalWeight > 0 ? ((dep.weight ?? 1) / totalWeight) * 100 : 0;
return <div key={dep.model_id + i} style={{ width: `${pct}%`, height: "100%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />;
})}
{isComplexityRouter ? (
<>
<div className="text-xs font-semibold text-gray-400 tracking-wide uppercase mb-1">2 &middot; Complexity Tiers</div>
<div className="text-xs text-gray-400 mb-3">Assign a model to each complexity tier. Simple queries go to cheaper models, complex ones to more capable models.</div>
<div className="flex flex-col gap-2.5">
{COMPLEXITY_TIERS.map((t) => (
<div key={t.key} className="rounded-lg border border-gray-200 bg-white px-4 py-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-semibold text-gray-800">{t.label}</span>
<span className="text-xs text-gray-400">{t.examples}</span>
</div>
<div className="text-xs text-gray-400 mb-2">{t.desc}</div>
<ModelSelector
accessToken={effectiveToken}
value={tierModels[t.key] || undefined}
placeholder={`Select model for ${t.label.toLowerCase()} tier...`}
showLabel={false}
onChange={(model) => handleTierModelChange(t.key, model)}
/>
</div>
))}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-1">
{deployments.map((dep, i) => {
</>
) : (
<>
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-gray-400 tracking-wide uppercase">2 &middot; Deployments</div>
<button
onClick={addEmptySlot}
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700 bg-transparent border-none cursor-pointer p-0 font-medium"
>
<PlusOutlined /> Add
</button>
</div>
{deployments.length > 0 && (
<div className="flex items-center flex-wrap gap-1 mb-3">
{deployments.map((dep, i) => {
const shortName = dep.model_name.split("/").pop() || dep.model_name;
return (
<React.Fragment key={dep.model_id + i}>
{i > 0 && <span style={{ fontSize: 10, color: "#9ca3af" }}>→</span>}
<span
style={{
fontSize: 11,
fontWeight: 600,
color: DEPLOY_COLORS[i % DEPLOY_COLORS.length],
backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] + "14",
border: `1px solid ${DEPLOY_COLORS[i % DEPLOY_COLORS.length]}30`,
padding: "2px 8px",
borderRadius: 6,
display: "inline-flex",
alignItems: "center",
gap: 4,
}}
>
<span style={{ fontWeight: 700, fontSize: 10 }}>{i + 1}</span>
{shortName}
</span>
</React.Fragment>
);
})}
</div>
)}
{strategy === "weighted" && deployments.length > 0 && (
<div className="mb-3">
<div style={{ width: "100%", height: 6, borderRadius: 3, overflow: "hidden", display: "flex", backgroundColor: "#e5e7eb" }}>
{deployments.map((dep, i) => {
const pct = totalWeight > 0 ? ((dep.weight ?? 1) / totalWeight) * 100 : 0;
return <div key={dep.model_id + i} style={{ width: `${pct}%`, height: "100%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />;
})}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-1">
{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 (
<div key={dep.model_id + i} className="flex items-center gap-1">
<div style={{ width: 6, height: 6, borderRadius: "50%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />
<span className="text-xs text-gray-500">{shortName}: {pct}%</span>
</div>
);
})}
</div>
</div>
)}
<div className="flex flex-col gap-2">
{deployments.map((dep, idx) => {
const pct = totalWeight > 0 ? Math.round(((dep.weight ?? 1) / totalWeight) * 100) : 0;
const shortName = dep.model_name.split("/").pop() || dep.model_name;
return (
<div key={dep.model_id + i} className="flex items-center gap-1">
<div style={{ width: 6, height: 6, borderRadius: "50%", backgroundColor: DEPLOY_COLORS[i % DEPLOY_COLORS.length] }} />
<span className="text-xs text-gray-500">{shortName}: {pct}%</span>
<div
key={dep.model_id + idx}
draggable
onDragStart={() => 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"
>
<div className="flex items-center gap-2">
<HolderOutlined className="text-gray-300 text-xs" />
<span className="text-xs font-bold text-gray-400 w-4 text-center">{idx + 1}</span>
<div className="flex-1 min-w-0" onClick={(e) => e.stopPropagation()}>
<ModelSelector
accessToken={effectiveToken}
value={dep.model_name}
placeholder="Select a model..."
showLabel={false}
onChange={(model) => handleDeploymentChange(idx, model)}
/>
</div>
<button
onClick={() => removeDeployment(idx)}
className="text-gray-300 hover:text-red-400 bg-transparent border-none cursor-pointer p-0 flex-shrink-0"
>
<DeleteOutlined style={{ fontSize: 13 }} />
</button>
</div>
{strategy === "weighted" && (
<div className="flex items-center gap-2 mt-2 ml-6" onClick={(e) => e.stopPropagation()}>
<InputNumber
size="small"
min={1}
max={99}
value={pct}
onChange={(val) => updatePercent(idx, val)}
addonAfter="%"
style={{ width: 80 }}
/>
<span className="text-xs text-gray-400">traffic</span>
</div>
)}
</div>
);
})}
{Array.from({ length: emptySlots }).map((_, slotIdx) => (
<div key={`slot-${slotIdx}`} className="rounded-lg border border-dashed border-gray-200 bg-gray-50 px-3 py-1.5">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-gray-300 w-4 text-center">{deployments.length + slotIdx + 1}</span>
<div className="flex-1">
<ModelSelector
accessToken={effectiveToken}
placeholder="Select a model..."
showLabel={false}
onChange={(model) => handleSlotSelected(slotIdx, model)}
/>
</div>
</div>
</div>
))}
</div>
</div>
</>
)}
<div className="flex flex-col gap-2">
{deployments.map((dep, idx) => {
const pct = totalWeight > 0 ? Math.round(((dep.weight ?? 1) / totalWeight) * 100) : 0;
return (
<div
key={dep.model_id + idx}
draggable
onDragStart={() => 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"
>
<div className="flex items-center gap-2">
<HolderOutlined className="text-gray-300 text-xs" />
<span className="text-xs font-bold text-gray-400 w-4 text-center">{idx + 1}</span>
<div className="flex-1 min-w-0" onClick={(e) => e.stopPropagation()}>
<ModelSelector
accessToken={effectiveToken}
value={dep.model_name}
placeholder="Select a model..."
showLabel={false}
onChange={(model) => handleDeploymentChange(idx, model)}
/>
</div>
<button
onClick={() => removeDeployment(idx)}
className="text-gray-300 hover:text-red-400 bg-transparent border-none cursor-pointer p-0 flex-shrink-0"
>
<DeleteOutlined style={{ fontSize: 13 }} />
</button>
</div>
{strategy === "weighted" && (
<div className="flex items-center gap-2 mt-2 ml-6" onClick={(e) => e.stopPropagation()}>
<span className="text-xs text-gray-500">Weight</span>
<InputNumber
size="small"
min={1}
max={100}
value={dep.weight ?? 1}
onChange={(val) => updateWeight(idx, val)}
addonAfter="%"
style={{ width: 90 }}
/>
<span className="text-xs text-gray-400">= {pct}% traffic</span>
</div>
)}
</div>
);
})}
{Array.from({ length: emptySlots }).map((_, slotIdx) => (
<div key={`slot-${slotIdx}`} className="rounded-lg border border-dashed border-gray-200 bg-gray-50 px-3 py-1.5">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-gray-300 w-4 text-center">{deployments.length + slotIdx + 1}</span>
<div className="flex-1">
<ModelSelector
accessToken={effectiveToken}
placeholder="Select a model..."
showLabel={false}
onChange={(model) => handleSlotSelected(slotIdx, model)}
/>
</div>
</div>
</div>
))}
</div>
</div>
{/* 3 · Logical Model Name */}
@ -455,28 +550,6 @@ export default function RoutingGroupBuilder({
</div>
</div>
{/* Stats footer */}
<div style={{ borderTop: "1px solid #e5e7eb", backgroundColor: "#fff", padding: "0 24px", flexShrink: 0 }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", borderBottom: "1px solid #e5e7eb" }}>
{[
{ label: "REQUESTS", value: "0" },
{ label: "SUCCESS", value: "100%", color: "#22c55e" },
{ label: "AVG LATENCY", value: "—" },
{ label: "FALLBACKS", value: "0" },
].map((stat, i) => (
<div key={stat.label} style={{ padding: "12px 0", borderLeft: i > 0 ? "1px solid #e5e7eb" : "none", paddingLeft: i > 0 ? 16 : 0 }}>
<div style={{ fontSize: 10, fontWeight: 600, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.05em" }}>{stat.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: stat.color ?? "#111827", marginTop: 2 }}>{stat.value}</div>
</div>
))}
</div>
<div className="flex items-center gap-2" style={{ padding: "10px 0" }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /><polyline points="14 2 14 8 20 8" />
</svg>
<span style={{ fontSize: 13, fontWeight: 500, color: "#374151" }}>Request Log</span>
</div>
</div>
</div>
</div>
</div>