diff --git a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx new file mode 100644 index 0000000000..db49e1d999 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx @@ -0,0 +1,342 @@ +import React, { useState, useEffect } from "react"; +import { message } from "antd"; +import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline"; +import { + Card, + Title, + Text, + Table, + TableHead, + TableHeaderCell, + TableBody, + TableRow, + TableCell +} from "@tremor/react"; +import ModelSelector from "./ModelSelector"; + +interface ModelAliasManagerProps { + accessToken: string; + initialModelAliases?: { [key: string]: string }; + onAliasUpdate?: (updatedAliases: { [key: string]: string }) => void; + showExampleConfig?: boolean; +} + +interface AliasItem { + id: string; + aliasName: string; + targetModel: string; +} + +const ModelAliasManager: React.FC = ({ + accessToken, + initialModelAliases = {}, + onAliasUpdate, + showExampleConfig = true, +}) => { + const [aliases, setAliases] = useState([]); + const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" }); + const [editingAlias, setEditingAlias] = useState(null); + + useEffect(() => { + // Convert object to array for display + const aliasArray = Object.entries(initialModelAliases).map(([aliasName, targetModel], index) => ({ + id: `${index}-${aliasName}`, + aliasName, + targetModel, + })); + setAliases(aliasArray); + }, [initialModelAliases]); + + const handleAddAlias = () => { + if (!newAlias.aliasName || !newAlias.targetModel) { + message.error("Please provide both alias name and target model"); + return; + } + + // Check for duplicate alias names + if (aliases.some(alias => alias.aliasName === newAlias.aliasName)) { + message.error("An alias with this name already exists"); + return; + } + + const newAliasObj: AliasItem = { + id: `${Date.now()}-${newAlias.aliasName}`, + aliasName: newAlias.aliasName, + targetModel: newAlias.targetModel, + }; + + const updatedAliases = [...aliases, newAliasObj]; + setAliases(updatedAliases); + setNewAlias({ aliasName: "", targetModel: "" }); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias added successfully"); + }; + + const handleEditAlias = (alias: AliasItem) => { + setEditingAlias({ ...alias }); + }; + + const handleUpdateAlias = () => { + if (!editingAlias) return; + + if (!editingAlias.aliasName || !editingAlias.targetModel) { + message.error("Please provide both alias name and target model"); + return; + } + + // Check for duplicate alias names (excluding current alias) + if (aliases.some(alias => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) { + message.error("An alias with this name already exists"); + return; + } + + const updatedAliases = aliases.map(alias => + alias.id === editingAlias.id ? editingAlias : alias + ); + + setAliases(updatedAliases); + setEditingAlias(null); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias updated successfully"); + }; + + const handleCancelEdit = () => { + setEditingAlias(null); + }; + + const deleteAlias = (aliasId: string) => { + const updatedAliases = aliases.filter(alias => alias.id !== aliasId); + setAliases(updatedAliases); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias deleted successfully"); + }; + + // Convert current aliases to object for config example + const aliasObject = aliases.reduce((acc, alias) => { + acc[alias.aliasName] = alias.targetModel; + return acc; + }, {} as { [key: string]: string }); + + return ( +
+
+ Add New Alias +
+
+ + + setNewAlias({ + ...newAlias, + aliasName: e.target.value, + }) + } + placeholder="e.g., gpt-4o" + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" + /> +
+
+ + + setNewAlias({ + ...newAlias, + targetModel: value, + }) + } + showLabel={false} + /> +
+
+ +
+
+
+ + + Manage Existing Aliases + +
+
+ + + + + Alias Name + + + Target Model + + + Actions + + + + + {aliases.map((alias) => ( + + {editingAlias && editingAlias.id === alias.id ? ( + <> + + + setEditingAlias({ + ...editingAlias, + aliasName: e.target.value, + }) + } + className="w-full px-2 py-1 border border-gray-300 rounded-md text-sm" + /> + + + + setEditingAlias({ + ...editingAlias, + targetModel: value, + }) + } + showLabel={false} + style={{ height: '32px' }} + /> + + +
+ + +
+
+ + ) : ( + <> + + {alias.aliasName} + + + {alias.targetModel} + + +
+ + +
+
+ + )} +
+ ))} + {aliases.length === 0 && ( + + + No aliases added yet. Add a new alias above. + + + )} +
+
+
+
+ + {/* Configuration Example */} + {showExampleConfig && ( + + Configuration Example + + Here's how your current aliases would look in the config: + +
+
+ model_aliases: + {Object.keys(aliasObject).length === 0 ? ( + +
+   # No aliases configured yet +
+ ) : ( + Object.entries(aliasObject).map(([key, value]) => ( + +
+   "{key}": "{value}" +
+ )) + )} +
+
+
+ )} +
+ ); +}; + +export default ModelAliasManager; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx new file mode 100644 index 0000000000..a8a8087cb6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx @@ -0,0 +1,122 @@ +import React, { useState, useEffect, useRef } from "react"; +import { TextInput, Text } from "@tremor/react"; +import { Select } from "antd"; +import { RobotOutlined } from "@ant-design/icons"; +import { fetchAvailableModels, ModelGroup } from "../chat_ui/llm_calls/fetch_models"; + +interface ModelSelectorProps { + accessToken: string; + value?: string; + placeholder?: string; + onChange?: (value: string) => void; + disabled?: boolean; + style?: React.CSSProperties; + className?: string; + showLabel?: boolean; + labelText?: string; +} + +const ModelSelector: React.FC = ({ + accessToken, + value, + placeholder = "Select a Model", + onChange, + disabled = false, + style, + className, + showLabel = true, + labelText = "Select Model" +}) => { + const [selectedModel, setSelectedModel] = useState(value); + const [showCustomModelInput, setShowCustomModelInput] = useState(false); + const [modelInfo, setModelInfo] = useState([]); + const customModelTimeout = useRef(null); + + useEffect(() => { + setSelectedModel(value); + }, [value]); + + useEffect(() => { + if (!accessToken) return; + + const loadModels = async () => { + try { + const uniqueModels = await fetchAvailableModels(accessToken); + console.log("Fetched models for selector:", uniqueModels); + + if (uniqueModels.length > 0) { + setModelInfo(uniqueModels); + } + } catch (error) { + console.error("Error fetching model info:", error); + } + }; + + loadModels(); + }, [accessToken]); + + const onModelChange = (value: string) => { + if (value === 'custom') { + setShowCustomModelInput(true); + setSelectedModel(undefined); + } else { + setShowCustomModelInput(false); + setSelectedModel(value); + if (onChange) { + onChange(value); + } + } + }; + + const handleCustomModelChange = (value: string) => { + // Using setTimeout to create a simple debounce effect + if (customModelTimeout.current) { + clearTimeout(customModelTimeout.current); + } + + customModelTimeout.current = setTimeout(() => { + setSelectedModel(value); + if (onChange) { + onChange(value); + } + }, 500); // 500ms delay after typing stops + }; + + return ( +
+ {showLabel && ( + + {labelText} + + )} +