[UI] - Add ability to set model alias per key/team (#13276)

* update model alias on keys

* team model aliases

* fix model aliases

* fixes for teams
This commit is contained in:
Ishaan Jaff
2025-08-04 21:47:39 -07:00
committed by GitHub
parent efd34966dc
commit ec205fec35
4 changed files with 525 additions and 0 deletions
@@ -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<ModelAliasManagerProps> = ({
accessToken,
initialModelAliases = {},
onAliasUpdate,
showExampleConfig = true,
}) => {
const [aliases, setAliases] = useState<AliasItem[]>([]);
const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" });
const [editingAlias, setEditingAlias] = useState<AliasItem | null>(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 (
<div className="mt-4">
<div className="mb-6">
<Text className="text-sm font-medium text-gray-700 mb-2">Add New Alias</Text>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">Alias Name</label>
<input
type="text"
value={newAlias.aliasName}
onChange={(e) =>
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"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">
Target Model
</label>
<ModelSelector
accessToken={accessToken}
value={newAlias.targetModel}
placeholder="Select target model"
onChange={(value) =>
setNewAlias({
...newAlias,
targetModel: value,
})
}
showLabel={false}
/>
</div>
<div className="flex items-end">
<button
onClick={handleAddAlias}
disabled={!newAlias.aliasName || !newAlias.targetModel}
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-green-600 text-white hover:bg-green-700'}`}
>
<PlusCircleIcon className="w-4 h-4 mr-1" />
Add Alias
</button>
</div>
</div>
</div>
<Text className="text-sm font-medium text-gray-700 mb-2">
Manage Existing Aliases
</Text>
<div className="rounded-lg custom-border relative mb-6">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
<TableRow>
<TableHeaderCell className="py-1 h-8">
Alias Name
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
Target Model
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
Actions
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{aliases.map((alias) => (
<TableRow key={alias.id} className="h-8">
{editingAlias && editingAlias.id === alias.id ? (
<>
<TableCell className="py-0.5">
<input
type="text"
value={editingAlias.aliasName}
onChange={(e) =>
setEditingAlias({
...editingAlias,
aliasName: e.target.value,
})
}
className="w-full px-2 py-1 border border-gray-300 rounded-md text-sm"
/>
</TableCell>
<TableCell className="py-0.5">
<ModelSelector
accessToken={accessToken}
value={editingAlias.targetModel}
onChange={(value) =>
setEditingAlias({
...editingAlias,
targetModel: value,
})
}
showLabel={false}
style={{ height: '32px' }}
/>
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
onClick={handleUpdateAlias}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
>
Save
</button>
<button
onClick={handleCancelEdit}
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100"
>
Cancel
</button>
</div>
</TableCell>
</>
) : (
<>
<TableCell className="py-0.5 text-sm text-gray-900">
{alias.aliasName}
</TableCell>
<TableCell className="py-0.5 text-sm text-gray-500">
{alias.targetModel}
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
onClick={() => handleEditAlias(alias)}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
>
<PencilIcon className="w-3 h-3" />
</button>
<button
onClick={() => deleteAlias(alias.id)}
className="text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100"
>
<TrashIcon className="w-3 h-3" />
</button>
</div>
</TableCell>
</>
)}
</TableRow>
))}
{aliases.length === 0 && (
<TableRow>
<TableCell
colSpan={3}
className="py-0.5 text-sm text-gray-500 text-center"
>
No aliases added yet. Add a new alias above.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
{/* Configuration Example */}
{showExampleConfig && (
<Card>
<Title className="mb-4">Configuration Example</Title>
<Text className="text-gray-600 mb-4">
Here&apos;s how your current aliases would look in the config:
</Text>
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-700">
model_aliases:
{Object.keys(aliasObject).length === 0 ? (
<span className="text-gray-500">
<br />
&nbsp;&nbsp;# No aliases configured yet
</span>
) : (
Object.entries(aliasObject).map(([key, value]) => (
<span key={key}>
<br />
&nbsp;&nbsp;&quot;{key}&quot;: &quot;{value}&quot;
</span>
))
)}
</div>
</div>
</Card>
)}
</div>
);
};
export default ModelAliasManager;
@@ -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<ModelSelectorProps> = ({
accessToken,
value,
placeholder = "Select a Model",
onChange,
disabled = false,
style,
className,
showLabel = true,
labelText = "Select Model"
}) => {
const [selectedModel, setSelectedModel] = useState<string | undefined>(value);
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const customModelTimeout = useRef<NodeJS.Timeout | null>(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 (
<div>
{showLabel && (
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<RobotOutlined className="mr-2" /> {labelText}
</Text>
)}
<Select
value={selectedModel}
placeholder={placeholder}
onChange={onModelChange}
options={[
...Array.from(new Set(modelInfo.map(option => option.model_group)))
.map((model_group, index) => ({
value: model_group,
label: model_group,
key: index
})),
{ value: 'custom', label: 'Enter custom model', key: 'custom' }
]}
style={{ width: "100%", ...style }}
showSearch={true}
className={`rounded-md ${className || ''}`}
disabled={disabled}
/>
{showCustomModelInput && (
<TextInput
className="mt-2"
placeholder="Enter custom model name"
onValueChange={handleCustomModelChange}
disabled={disabled}
/>
)}
</div>
);
};
export default ModelSelector;
@@ -49,6 +49,7 @@ import BudgetDurationDropdown from "./common_components/budget_duration_dropdown
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { callback_map, mapDisplayToInternalNames } from "./callback_info_helpers";
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import ModelAliasManager from "./common_components/ModelAliasManager";
const { Option } = Select;
@@ -184,6 +185,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [disabledCallbacks, setDisabledCallbacks] = useState<string[]>([]);
const [keyType, setKeyType] = useState<string>("default");
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
const handleOk = () => {
setIsModalVisible(false);
@@ -191,6 +193,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
setLoggingSettings([]);
setDisabledCallbacks([]);
setKeyType("default");
setModelAliases({});
};
const handleCancel = () => {
@@ -201,6 +204,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
setLoggingSettings([]);
setDisabledCallbacks([]);
setKeyType("default");
setModelAliases({});
};
useEffect(() => {
@@ -371,6 +375,12 @@ const CreateKey: React.FC<CreateKeyProps> = ({
// Remove the original field as it's now part of object_permission
delete formValues.allowed_mcp_access_groups;
}
// Add model_aliases if any are defined
if (Object.keys(modelAliases).length > 0) {
formValues.aliases = JSON.stringify(modelAliases);
}
let response;
if (keyOwner === "service_account") {
response = await keyCreateServiceAccountCall(accessToken, formValues);
@@ -1008,6 +1018,25 @@ const CreateKey: React.FC<CreateKeyProps> = ({
</div>
</AccordionBody>
</Accordion>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<b>Model Aliases</b>
</AccordionHeader>
<AccordionBody>
<div className="mt-4">
<Text className="text-sm text-gray-600 mb-4">
Create custom aliases for models that can be used in API calls. This allows you to create shortcuts for specific models.
</Text>
<ModelAliasManager
accessToken={accessToken}
initialModelAliases={modelAliases}
onAliasUpdate={setModelAliases}
showExampleConfig={false}
/>
</div>
</AccordionBody>
</Accordion>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<div className="flex items-center gap-2">
@@ -74,6 +74,7 @@ import type { KeyResponse, Team } from "./key_team_helpers/key_list";
import { formatNumberWithCommas } from "../utils/dataUtils";
import { AlertTriangleIcon, XIcon } from 'lucide-react';
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import ModelAliasManager from "./common_components/ModelAliasManager";
interface TeamProps {
teams: Team[] | null;
@@ -205,6 +206,7 @@ const Teams: React.FC<TeamProps> = ({
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
useEffect(() => {
console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
@@ -278,6 +280,7 @@ const Teams: React.FC<TeamProps> = ({
setIsTeamModalVisible(false);
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
};
const handleMemberOk = () => {
@@ -290,6 +293,7 @@ const Teams: React.FC<TeamProps> = ({
setIsTeamModalVisible(false);
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
};
const handleMemberCancel = () => {
@@ -439,6 +443,12 @@ const Teams: React.FC<TeamProps> = ({
formValues.allowed_mcp_access_groups;
delete formValues.allowed_mcp_access_groups;
}
// Add model_aliases if any are defined
if (Object.keys(modelAliases).length > 0) {
formValues.model_aliases = modelAliases;
}
const response: any = await teamCreateCall(accessToken, formValues);
if (teams !== null) {
setTeams([...teams, response]);
@@ -449,6 +459,7 @@ const Teams: React.FC<TeamProps> = ({
message.success("Team created");
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
setIsTeamModalVisible(false);
}
} catch (error) {
@@ -1466,6 +1477,7 @@ const Teams: React.FC<TeamProps> = ({
placeholder="Select MCP servers or access groups (optional)"
/>
</Form.Item>
</AccordionBody>
</Accordion>
@@ -1483,6 +1495,26 @@ const Teams: React.FC<TeamProps> = ({
</div>
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Model Aliases</b>
</AccordionHeader>
<AccordionBody>
<div className="mt-4">
<Text className="text-sm text-gray-600 mb-4">
Create custom aliases for models that can be used by team members in API calls. This allows you to create shortcuts for specific models.
</Text>
<ModelAliasManager
accessToken={accessToken || ""}
initialModelAliases={modelAliases}
onAliasUpdate={setModelAliases}
showExampleConfig={false}
/>
</div>
</AccordionBody>
</Accordion>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Create Team</Button2>