mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
ui fixes
This commit is contained in:
parent
f39adbc50b
commit
4611f8f889
3 changed files with 381 additions and 341 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 · no API calls · <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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 · 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 · 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 · 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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue