mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fixes
This commit is contained in:
parent
4255bc1136
commit
b5b0b77e0e
5 changed files with 565 additions and 244 deletions
|
|
@ -37,6 +37,7 @@ import UIThemeSettings from "@/components/ui_theme_settings";
|
|||
import Usage from "@/components/usage";
|
||||
import UserDashboard from "@/components/user_dashboard";
|
||||
import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage";
|
||||
import RoutingGroupsView from "@/components/routing_groups/RoutingGroupsView";
|
||||
import { ProjectsPage } from "@/components/Projects/ProjectsPage";
|
||||
import VectorStoreManagement from "@/components/vector_store_management";
|
||||
import ToolPolicies from "@/components/ToolPolicies";
|
||||
|
|
@ -430,6 +431,12 @@ function CreateKeyPageContent() {
|
|||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : page == "routing-groups" ? (
|
||||
<RoutingGroupsView
|
||||
accessToken={accessToken}
|
||||
userRole={userRole ?? ""}
|
||||
userId={userID ?? ""}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<OldModelDashboard
|
||||
token={token}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
BgColorsOutlined,
|
||||
BlockOutlined,
|
||||
BookOutlined,
|
||||
BranchesOutlined,
|
||||
CreditCardOutlined,
|
||||
DatabaseOutlined,
|
||||
ExperimentOutlined,
|
||||
|
|
@ -87,6 +88,13 @@ const menuGroups: MenuGroup[] = [
|
|||
icon: <BlockOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "routing-groups",
|
||||
page: "routing-groups",
|
||||
label: "Routing Groups",
|
||||
icon: <BranchesOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "agents",
|
||||
page: "agents",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { Select } from "antd";
|
||||
import { RobotOutlined } from "@ant-design/icons";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { useRef, useState } from "react";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
|
||||
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
|
||||
import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer";
|
||||
|
||||
export interface DeploymentOption {
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface DeploymentSelectorProps {
|
||||
value?: string | null;
|
||||
placeholder?: string;
|
||||
onChange?: (deployment: DeploymentOption | null) => void;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
showLabel?: boolean;
|
||||
labelText?: string;
|
||||
excludeIds?: Set<string>;
|
||||
}
|
||||
|
||||
export default function DeploymentSelector({
|
||||
value,
|
||||
placeholder = "Select a deployment",
|
||||
onChange,
|
||||
disabled = false,
|
||||
style,
|
||||
className,
|
||||
showLabel = true,
|
||||
labelText = "Select Model",
|
||||
excludeIds = new Set(),
|
||||
}: DeploymentSelectorProps) {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
const { data: rawModelData } = useModelsInfo(1, 200);
|
||||
const { data: modelCostMapData } = useModelCostMap();
|
||||
const [selectedValue, setSelectedValue] = useState<string | undefined>(value ?? undefined);
|
||||
const [showCustomModelInput, setShowCustomModelInput] = useState(false);
|
||||
const customModelTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedValue(value ?? undefined);
|
||||
}, [value]);
|
||||
|
||||
const getProviderFromModel = (model: string) => {
|
||||
if (modelCostMapData && typeof modelCostMapData === "object" && model in modelCostMapData) {
|
||||
return modelCostMapData[model]?.litellm_provider ?? "openai";
|
||||
}
|
||||
return "openai";
|
||||
};
|
||||
|
||||
const modelData = useMemo(() => {
|
||||
if (!rawModelData?.data) return { data: [] };
|
||||
return transformModelData(rawModelData, getProviderFromModel);
|
||||
}, [rawModelData?.data, modelCostMapData]);
|
||||
|
||||
const deploymentOptions = useMemo(() => {
|
||||
if (!modelData?.data) return [];
|
||||
return modelData.data
|
||||
.filter((m: any) => m.model_info?.id && !excludeIds.has(m.model_info.id))
|
||||
.map((m: any) => ({
|
||||
model_id: m.model_info.id,
|
||||
model_name: m.model_name,
|
||||
provider: m.provider || "-",
|
||||
label: `${m.provider || "Unknown"} / ${m.model_name}`,
|
||||
}));
|
||||
}, [modelData?.data, excludeIds]);
|
||||
|
||||
const options = [
|
||||
{ value: "__custom__", label: "Enter custom model", key: "custom" },
|
||||
...deploymentOptions.map((d) => ({
|
||||
value: d.model_id,
|
||||
label: d.label,
|
||||
key: d.model_id,
|
||||
})),
|
||||
];
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
if (val === "__custom__") {
|
||||
setShowCustomModelInput(true);
|
||||
setSelectedValue(undefined);
|
||||
onChange?.(null);
|
||||
} else {
|
||||
setShowCustomModelInput(false);
|
||||
setSelectedValue(val);
|
||||
const dep = deploymentOptions.find((d) => d.model_id === val);
|
||||
if (dep) {
|
||||
onChange?.({ model_id: dep.model_id, model_name: dep.model_name, provider: dep.provider });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomValue = !!value && !deploymentOptions.some((d) => d.model_id === value);
|
||||
|
||||
const handleCustomModelChange = (inputVal: string) => {
|
||||
if (customModelTimeout.current) clearTimeout(customModelTimeout.current);
|
||||
customModelTimeout.current = setTimeout(() => {
|
||||
if (inputVal.trim()) {
|
||||
setSelectedValue(`custom:${inputVal}`);
|
||||
onChange?.({
|
||||
model_id: inputVal.trim(),
|
||||
model_name: inputVal.trim(),
|
||||
provider: "custom",
|
||||
});
|
||||
} else {
|
||||
onChange?.(null);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showLabel && (
|
||||
<div className="font-medium mb-2 text-gray-700 flex items-center">
|
||||
<RobotOutlined className="mr-2" /> {labelText}
|
||||
</div>
|
||||
)}
|
||||
<Select
|
||||
value={showCustomModelInput ? undefined : selectedValue}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
style={{ width: "100%", ...style }}
|
||||
showSearch
|
||||
className={`rounded-md ${className || ""}`}
|
||||
disabled={disabled}
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toString().toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
{(showCustomModelInput || isCustomValue) && (
|
||||
<TextInput
|
||||
className="mt-2"
|
||||
placeholder="Enter custom model name (e.g. openai/gpt-4)"
|
||||
defaultValue={isCustomValue ? value : undefined}
|
||||
onValueChange={handleCustomModelChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,110 +1,127 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Checkbox, Input, InputNumber, Typography } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
ArrowLeftOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
BranchesOutlined,
|
||||
CloudOutlined,
|
||||
CodeOutlined,
|
||||
DeleteOutlined,
|
||||
OrderedListOutlined,
|
||||
PlusOutlined,
|
||||
PercentageOutlined,
|
||||
ThunderboltOutlined,
|
||||
DashboardOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { routingGroupCreateCall, routingGroupUpdateCall } from "@/components/networking";
|
||||
import DeploymentSelector, { DeploymentOption } from "./DeploymentSelector";
|
||||
|
||||
interface Deployment {
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
litellm_provider: string;
|
||||
provider: string;
|
||||
weight?: number;
|
||||
priority?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface RoutingGroupBuilderProps {
|
||||
accessToken: string | null;
|
||||
visible: boolean;
|
||||
editTarget: Record<string, unknown> | null;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ROUTING_STRATEGIES = [
|
||||
{ value: "priority-failover", label: "Priority Failover: ordered, first success wins" },
|
||||
{ value: "weighted", label: "Weighted: split traffic by percentage" },
|
||||
{ value: "cost-based-routing", label: "Cost-Based: route to cheapest" },
|
||||
{ value: "latency-based-routing", label: "Latency-Based: route to fastest" },
|
||||
{ value: "least-busy", label: "Least Busy: fewest in-flight requests" },
|
||||
{ value: "usage-based-routing-v2", label: "Usage-Based: TPM/RPM aware" },
|
||||
{ value: "simple-shuffle", label: "Round Robin: even distribution" },
|
||||
const PROVIDER_COLORS: Record<string, string> = {
|
||||
"Nebius": "#10b981",
|
||||
"Fireworks AI": "#ef4444",
|
||||
"Azure": "#3b82f6",
|
||||
"Azure OpenAI": "#3b82f6",
|
||||
"OpenAI": "#10b981",
|
||||
"Anthropic": "#f59e0b",
|
||||
"Google AI Studio": "#4285f4",
|
||||
"Groq": "#8b5cf6",
|
||||
"Deepseek": "#06b6d4",
|
||||
"Mistral AI": "#f97316",
|
||||
"Amazon Bedrock": "#f59e0b",
|
||||
"custom": "#6b7280",
|
||||
};
|
||||
|
||||
function getProviderColor(provider: string): string {
|
||||
return PROVIDER_COLORS[provider] ?? "#6366f1";
|
||||
}
|
||||
|
||||
function getProviderInitial(provider: string): string {
|
||||
if (provider === "-" || !provider) return "?";
|
||||
return provider.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
const STRATEGIES = [
|
||||
{
|
||||
value: "priority-failover",
|
||||
label: "Ordered Fallback",
|
||||
desc: "Try in priority order, skip on failure",
|
||||
icon: <OrderedListOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
value: "weighted",
|
||||
label: "Weighted Round-Robin",
|
||||
desc: "Distribute traffic by weight %",
|
||||
icon: <PercentageOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
value: "usage-based-routing-v2",
|
||||
label: "Usage-Based",
|
||||
desc: "Route to least-loaded deployment",
|
||||
icon: <DashboardOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
value: "latency-based-routing",
|
||||
label: "Latency-Based",
|
||||
desc: "Route to fastest responding backend",
|
||||
icon: <ThunderboltOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function RoutingGroupBuilder({
|
||||
accessToken,
|
||||
visible,
|
||||
editTarget,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: RoutingGroupBuilderProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [strategy, setStrategy] = useState("priority-failover");
|
||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||
const [newModelName, setNewModelName] = useState("");
|
||||
const [newProvider, setNewProvider] = useState("");
|
||||
const [logicalName, setLogicalName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [strategy, setStrategy] = useState<string>("priority-failover");
|
||||
const [showAddSelector, setShowAddSelector] = useState(false);
|
||||
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentOption | null>(null);
|
||||
const [selectorKey, setSelectorKey] = useState(0);
|
||||
|
||||
const isEditing = editTarget !== null;
|
||||
|
||||
useEffect(() => {
|
||||
if (editTarget) {
|
||||
form.setFieldsValue({
|
||||
routing_group_name: editTarget.routing_group_name,
|
||||
description: editTarget.description,
|
||||
routing_strategy: editTarget.routing_strategy ?? "priority-failover",
|
||||
max_retries: editTarget.max_retries,
|
||||
cooldown_time: editTarget.cooldown_time,
|
||||
});
|
||||
setStrategy((editTarget.routing_strategy as string) ?? "priority-failover");
|
||||
setLogicalName((editTarget.routing_group_name as string) ?? "");
|
||||
setDescription((editTarget.description as string) ?? "");
|
||||
if (Array.isArray(editTarget.deployments)) {
|
||||
setDeployments(editTarget.deployments as Deployment[]);
|
||||
setDeployments(
|
||||
(editTarget.deployments as any[]).map((d, i) => ({
|
||||
model_id: d.model_id ?? d.model_name,
|
||||
model_name: d.model_name,
|
||||
provider: d.provider ?? d.litellm_provider ?? "-",
|
||||
weight: d.weight,
|
||||
priority: d.priority ?? i + 1,
|
||||
enabled: true,
|
||||
}))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
setDeployments([]);
|
||||
setStrategy("priority-failover");
|
||||
}
|
||||
}, [editTarget, form]);
|
||||
|
||||
const addDeployment = () => {
|
||||
const trimmedModel = newModelName.trim();
|
||||
if (!trimmedModel) return;
|
||||
const newDeployment: Deployment = {
|
||||
model_name: trimmedModel,
|
||||
litellm_provider: newProvider.trim(),
|
||||
weight: strategy === "weighted" ? 1 : undefined,
|
||||
priority: strategy === "priority-failover" ? deployments.length + 1 : undefined,
|
||||
};
|
||||
setDeployments((prev) => [...prev, newDeployment]);
|
||||
setNewModelName("");
|
||||
setNewProvider("");
|
||||
};
|
||||
|
||||
const removeDeployment = (index: number) => {
|
||||
setDeployments((prev) => {
|
||||
const updated = prev.filter((_, i) => i !== index);
|
||||
if (strategy === "priority-failover") {
|
||||
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const updateDeploymentWeight = (index: number, weight: number | null) => {
|
||||
setDeployments((prev) =>
|
||||
prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d))
|
||||
);
|
||||
};
|
||||
}, [editTarget]);
|
||||
|
||||
const handleStrategyChange = (value: string) => {
|
||||
setStrategy(value);
|
||||
|
|
@ -117,31 +134,74 @@ export default function RoutingGroupBuilder({
|
|||
);
|
||||
};
|
||||
|
||||
const addDeployment = () => {
|
||||
if (!selectedDeployment) return;
|
||||
setDeployments((prev) => [
|
||||
...prev,
|
||||
{
|
||||
model_id: selectedDeployment.model_id,
|
||||
model_name: selectedDeployment.model_name,
|
||||
provider: selectedDeployment.provider,
|
||||
weight: strategy === "weighted" ? 1 : undefined,
|
||||
priority: strategy === "priority-failover" ? prev.length + 1 : undefined,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
setSelectedDeployment(null);
|
||||
setSelectorKey((k) => k + 1);
|
||||
setShowAddSelector(false);
|
||||
};
|
||||
|
||||
const removeDeployment = (index: number) => {
|
||||
setDeployments((prev) => {
|
||||
const updated = prev.filter((_, i) => i !== index);
|
||||
if (strategy === "priority-failover") {
|
||||
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const moveDeployment = (index: number, direction: "up" | "down") => {
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= deployments.length) return;
|
||||
setDeployments((prev) => {
|
||||
const updated = [...prev];
|
||||
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
|
||||
if (strategy === "priority-failover") {
|
||||
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWeight = (index: number, weight: number | null) => {
|
||||
setDeployments((prev) =>
|
||||
prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d))
|
||||
);
|
||||
};
|
||||
|
||||
const toggleEnabled = (index: number) => {
|
||||
setDeployments((prev) =>
|
||||
prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d))
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken || !logicalName.trim() || deployments.length === 0) return;
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
routing_group_name: values.routing_group_name,
|
||||
description: values.description,
|
||||
routing_strategy: values.routing_strategy,
|
||||
deployments,
|
||||
max_retries: values.max_retries,
|
||||
cooldown_time: values.cooldown_time,
|
||||
routing_group_name: logicalName.trim(),
|
||||
description: description.trim() || undefined,
|
||||
routing_strategy: strategy,
|
||||
deployments: deployments.filter((d) => d.enabled !== false),
|
||||
};
|
||||
|
||||
if (isEditing && editTarget?.routing_group_id) {
|
||||
await routingGroupUpdateCall(
|
||||
accessToken,
|
||||
editTarget.routing_group_id as string,
|
||||
payload
|
||||
);
|
||||
await routingGroupUpdateCall(accessToken, editTarget.routing_group_id as string, payload);
|
||||
} else {
|
||||
await routingGroupCreateCall(accessToken, payload);
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.error("Failed to save routing group:", err);
|
||||
|
|
@ -150,163 +210,256 @@ export default function RoutingGroupBuilder({
|
|||
}
|
||||
};
|
||||
|
||||
const excludeIds = useMemo(() => new Set(deployments.map((d) => d.model_id)), [deployments]);
|
||||
|
||||
const codeSnippet = `# invoke this group:\nclient.chat.completions.create(\n model="${logicalName || "your-model-name"}",\n messages=[...]\n)`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEditing ? "Edit Routing Group" : "Create Routing Group"}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={handleSubmit}
|
||||
<div className="w-full max-w-2xl mx-auto py-6 px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-1">
|
||||
<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"
|
||||
>
|
||||
{isEditing ? "Save Changes" : "Create"}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ routing_strategy: "priority-failover" }}
|
||||
>
|
||||
<Form.Item
|
||||
name="routing_group_name"
|
||||
label="Name"
|
||||
rules={[{ required: true, message: "Please enter a name" }]}
|
||||
>
|
||||
<Input placeholder="e.g. production-gpt4-pool" />
|
||||
</Form.Item>
|
||||
<ArrowLeftOutlined /> Back to Routing Groups
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-gray-900 m-0">Routing Group Builder</h1>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-0">
|
||||
Configure a named model group with a routing strategy
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input.TextArea rows={2} placeholder="Optional description" />
|
||||
</Form.Item>
|
||||
{/* Section 1: Routing Strategy */}
|
||||
<div className="mt-8">
|
||||
<div className="text-xs font-semibold text-gray-500 tracking-wide mb-4">
|
||||
1 · ROUTING STRATEGY
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{STRATEGIES.map((s) => {
|
||||
const isActive = strategy === s.value;
|
||||
return (
|
||||
<button
|
||||
key={s.value}
|
||||
onClick={() => handleStrategyChange(s.value)}
|
||||
className="text-left p-4 rounded-xl border-2 transition-all bg-white cursor-pointer"
|
||||
style={{
|
||||
borderColor: isActive ? "#3b82f6" : "#e5e7eb",
|
||||
boxShadow: isActive ? "0 0 0 1px #3b82f6" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="mb-2 text-gray-400">{s.icon}</div>
|
||||
<div className="font-semibold text-gray-900 text-sm">{s.label}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{s.desc}</div>
|
||||
{isActive && (
|
||||
<span className="inline-block mt-2 text-xs font-semibold text-blue-600 border border-blue-600 rounded px-1.5 py-0.5">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="routing_strategy"
|
||||
label="Routing Strategy"
|
||||
rules={[{ required: true, message: "Please select a strategy" }]}
|
||||
>
|
||||
<Radio.Group
|
||||
onChange={(e) => handleStrategyChange(e.target.value)}
|
||||
>
|
||||
<Space direction="vertical">
|
||||
{ROUTING_STRATEGIES.map((s) => (
|
||||
<Radio key={s.value} value={s.value}>
|
||||
{s.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text strong>Deployments</Typography.Text>
|
||||
<div style={{ marginTop: 8, marginBottom: 8 }}>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="Model name (e.g. gpt-4)"
|
||||
value={newModelName}
|
||||
onChange={(e) => setNewModelName(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
onPressEnter={addDeployment}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Provider (e.g. openai)"
|
||||
value={newProvider}
|
||||
onChange={(e) => setNewProvider(e.target.value)}
|
||||
style={{ width: 160 }}
|
||||
onPressEnter={addDeployment}
|
||||
/>
|
||||
<Button icon={<PlusOutlined />} onClick={addDeployment}>
|
||||
Add Deployment
|
||||
</Button>
|
||||
</Space>
|
||||
{/* Section 2: Deployments */}
|
||||
<div className="mt-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-xs font-semibold text-gray-500 tracking-wide">
|
||||
2 · DEPLOYMENTS
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddSelector(!showAddSelector)}
|
||||
className="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 bg-transparent border-none cursor-pointer p-0"
|
||||
>
|
||||
<PlusOutlined /> Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{deployments.length === 0 && (
|
||||
<Typography.Text type="secondary">
|
||||
No deployments added yet.
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{deployments.map((dep, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
padding: "6px 10px",
|
||||
background: "#fafafa",
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{strategy === "priority-failover" && (
|
||||
<Typography.Text type="secondary" style={{ minWidth: 24 }}>
|
||||
#{dep.priority}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Typography.Text style={{ flex: 1 }}>
|
||||
{dep.model_name}
|
||||
{dep.litellm_provider ? ` (${dep.litellm_provider})` : ""}
|
||||
</Typography.Text>
|
||||
{strategy === "weighted" && (
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={1}
|
||||
max={100}
|
||||
value={dep.weight ?? 1}
|
||||
onChange={(val) => updateDeploymentWeight(idx, val)}
|
||||
addonAfter="%"
|
||||
style={{ width: 100 }}
|
||||
{/* Add selector */}
|
||||
{showAddSelector && (
|
||||
<div className="mb-4 p-3 rounded-lg border border-blue-200 bg-blue-50">
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<DeploymentSelector
|
||||
key={selectorKey}
|
||||
value={selectedDeployment?.model_id ?? null}
|
||||
placeholder="Search and select a model..."
|
||||
showLabel={false}
|
||||
excludeIds={excludeIds}
|
||||
onChange={(dep) => setSelectedDeployment(dep)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
disabled={!selectedDeployment}
|
||||
onClick={addDeployment}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => removeDeployment(idx)}
|
||||
/>
|
||||
onClick={() => { setShowAddSelector(false); setSelectedDeployment(null); }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pipeline visualization */}
|
||||
{deployments.length > 0 && (
|
||||
<div className="flex items-center gap-1 mb-4 flex-wrap">
|
||||
{deployments.map((dep, idx) => {
|
||||
const color = getProviderColor(dep.provider);
|
||||
return (
|
||||
<React.Fragment key={dep.model_id}>
|
||||
{idx > 0 && (
|
||||
<span className="text-gray-300 text-xs mx-0.5">→</span>
|
||||
)}
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-xs font-medium rounded-full px-2.5 py-1 border"
|
||||
style={{
|
||||
borderColor: color + "44",
|
||||
color: color,
|
||||
backgroundColor: color + "0d",
|
||||
}}
|
||||
>
|
||||
<span className="font-bold">{idx + 1}</span>
|
||||
{dep.provider !== "-" ? dep.provider.split(" ")[0] : dep.model_name}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deployment cards */}
|
||||
{deployments.length === 0 && !showAddSelector && (
|
||||
<div className="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-200 rounded-xl">
|
||||
No deployments yet. Click <strong>+ Add</strong> to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{deployments.map((dep, idx) => {
|
||||
const color = getProviderColor(dep.provider);
|
||||
return (
|
||||
<div
|
||||
key={dep.model_id}
|
||||
className="flex items-center gap-3 rounded-xl border bg-white px-4 py-3 transition-all"
|
||||
style={{ borderLeft: `4px solid ${color}` }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={dep.enabled !== false}
|
||||
onChange={() => toggleEnabled(idx)}
|
||||
/>
|
||||
<span className="text-xs font-bold text-gray-400 min-w-[16px]">
|
||||
{idx + 1}
|
||||
</span>
|
||||
{/* Provider icon */}
|
||||
<div
|
||||
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg text-xs font-bold"
|
||||
style={{ backgroundColor: color + "1a", color }}
|
||||
>
|
||||
{getProviderInitial(dep.provider)}
|
||||
</div>
|
||||
{/* Name */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
{dep.provider}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 ml-1.5">
|
||||
/ {dep.model_name}
|
||||
</span>
|
||||
</div>
|
||||
{/* Weight (only for weighted) */}
|
||||
{strategy === "weighted" && (
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={1}
|
||||
max={100}
|
||||
value={dep.weight ?? 1}
|
||||
onChange={(val) => updateWeight(idx, val)}
|
||||
addonAfter="%"
|
||||
style={{ width: 90 }}
|
||||
/>
|
||||
)}
|
||||
{/* Reorder */}
|
||||
<div className="flex flex-col gap-0">
|
||||
<button
|
||||
onClick={() => moveDeployment(idx, "up")}
|
||||
disabled={idx === 0}
|
||||
className="text-gray-300 hover:text-gray-500 disabled:opacity-30 bg-transparent border-none cursor-pointer p-0 leading-none"
|
||||
>
|
||||
<ArrowUpOutlined style={{ fontSize: 12 }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveDeployment(idx, "down")}
|
||||
disabled={idx === deployments.length - 1}
|
||||
className="text-gray-300 hover:text-gray-500 disabled:opacity-30 bg-transparent border-none cursor-pointer p-0 leading-none"
|
||||
>
|
||||
<ArrowDownOutlined style={{ fontSize: 12 }} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Delete */}
|
||||
<button
|
||||
onClick={() => removeDeployment(idx)}
|
||||
className="text-gray-300 hover:text-red-400 bg-transparent border-none cursor-pointer p-0"
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: 14 }} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Logical Model Name */}
|
||||
<div className="mt-10">
|
||||
<div className="text-xs font-semibold text-gray-500 tracking-wide mb-1">
|
||||
3 · LOGICAL MODEL NAME
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-3">
|
||||
What callers use to invoke this group
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border border-gray-200 rounded-lg px-3 py-2 bg-white">
|
||||
<CodeOutlined className="text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={logicalName}
|
||||
onChange={(e) => setLogicalName(e.target.value)}
|
||||
placeholder="prod-model"
|
||||
className="flex-1 border-none outline-none text-sm font-mono text-gray-800 bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collapse ghost>
|
||||
<Collapse.Panel header="Advanced Settings" key="advanced">
|
||||
<Form.Item name="max_retries" label="Max Retries">
|
||||
<InputNumber min={0} max={100} style={{ width: 160 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cooldown_time" label="Cooldown Time (seconds)">
|
||||
<InputNumber min={0} style={{ width: 160 }} />
|
||||
</Form.Item>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
{/* Code snippet */}
|
||||
<div className="mt-3 rounded-lg bg-gray-900 px-4 py-3 font-mono text-xs text-gray-300 leading-relaxed overflow-x-auto">
|
||||
<pre className="m-0 whitespace-pre">{codeSnippet}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
{/* Save button */}
|
||||
<div className="mt-8 mb-4">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
disabled={!logicalName.trim() || deployments.length === 0}
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: "8px 12px",
|
||||
background: "#f6f8fa",
|
||||
borderRadius: 4,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary">
|
||||
Access is managed via Teams.{" "}
|
||||
<a href="/teams" target="_blank" rel="noreferrer">
|
||||
Manage Teams
|
||||
</a>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
{isEditing ? "Update Routing Group" : "Save Routing Group"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ export default function RoutingGroupsView({
|
|||
userRole,
|
||||
userId,
|
||||
}: RoutingGroupsViewProps) {
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [showBuilder, setShowBuilder] = useState(false);
|
||||
const [editTarget, setEditTarget] = useState<Record<string, unknown> | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [selectedGroup, setSelectedGroup] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
const handleEdit = (group: Record<string, unknown>) => {
|
||||
setEditTarget(group);
|
||||
setCreateModalVisible(true);
|
||||
setShowBuilder(true);
|
||||
setSelectedGroup(null);
|
||||
};
|
||||
|
||||
const handleTest = (group: Record<string, unknown>) => {
|
||||
|
|
@ -33,16 +34,29 @@ export default function RoutingGroupsView({
|
|||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setCreateModalVisible(false);
|
||||
setShowBuilder(false);
|
||||
setEditTarget(null);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setCreateModalVisible(false);
|
||||
setShowBuilder(false);
|
||||
setEditTarget(null);
|
||||
setRefreshKey((k) => k + 1);
|
||||
};
|
||||
|
||||
if (showBuilder) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<RoutingGroupBuilder
|
||||
accessToken={accessToken}
|
||||
editTarget={editTarget}
|
||||
onClose={handleClose}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
|
|
@ -57,7 +71,7 @@ export default function RoutingGroupsView({
|
|||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
onClick={() => setShowBuilder(true)}
|
||||
>
|
||||
Create Routing Group
|
||||
</Button>
|
||||
|
|
@ -84,7 +98,6 @@ export default function RoutingGroupsView({
|
|||
size="small"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setSelectedGroup(null)}
|
||||
style={{ color: "#64748b", borderColor: "#334155" }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
|
@ -97,16 +110,6 @@ export default function RoutingGroupsView({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{createModalVisible && (
|
||||
<RoutingGroupBuilder
|
||||
accessToken={accessToken}
|
||||
visible={createModalVisible}
|
||||
editTarget={editTarget}
|
||||
onClose={handleClose}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue