(UI) Allow adding models for a Team (#8598) (#8601)

* ui - use common team dropdown component

* re-use team component

* rename org field on add model

* handle add model submit

* working view model_id and team_id on root models page

* cleaner

* show all fields

* working model info view

* working team info selector

* clean up team id
This commit is contained in:
Ishaan Jaff
2025-02-17 17:57:56 -08:00
committed by GitHub
parent ce1d8026d9
commit 341a63cf8b
8 changed files with 1276 additions and 813 deletions
+1
View File
@@ -247,6 +247,7 @@ export default function CreateKeyPage() {
modelData={modelData}
setModelData={setModelData}
premiumUser={premiumUser}
teams={teams}
/>
) : page == "llm-playground" ? (
<ChatUI
@@ -3,16 +3,20 @@ import { Form, Switch, Select, Input } from "antd";
import { Text, Button, Accordion, AccordionHeader, AccordionBody, TextInput } from "@tremor/react";
import { Row, Col, Typography, Card } from "antd";
import TextArea from "antd/es/input/TextArea";
import { Team } from "../key_team_helpers/key_list";
import TeamDropdown from "../common_components/team_dropdown";
const { Link } = Typography;
interface AdvancedSettingsProps {
showAdvancedSettings: boolean;
setShowAdvancedSettings: (show: boolean) => void;
teams?: Team[] | null;
}
const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
showAdvancedSettings,
setShowAdvancedSettings,
teams,
}) => {
const [form] = Form.useForm();
const [customPricing, setCustomPricing] = React.useState(false);
@@ -87,6 +91,14 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
</AccordionHeader>
<AccordionBody>
<div className="bg-white rounded-lg">
<Form.Item
label="Team"
name="team_id"
className="mb-4"
>
<TeamDropdown teams={teams} />
</Form.Item>
<Form.Item
label="Custom Pricing"
name="custom_pricing"
@@ -76,6 +76,9 @@ export const handleAddModelSubmit = async (
// Add key-value pair to model_info dictionary
modelInfoObj[key] = value;
}
else if (key === "team_id") {
modelInfoObj["team_id"] = value;
}
else if (key === "custom_model_name") {
litellmParamsObj["model"] = value;
} else if (key == "litellm_extra_params") {
@@ -24,7 +24,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
return (
<>
{selectedProviderEnum === Providers.OpenAI && (
<Form.Item label="Organization ID" name="organization">
<Form.Item label="OpenAI Organization ID" name="organization">
<TextInput placeholder="[OPTIONAL] my-unique-org" />
</Form.Item>
)}
@@ -0,0 +1,35 @@
import React from "react";
import { Select } from "antd";
import { Team } from "../key_team_helpers/key_list";
interface TeamDropdownProps {
teams?: Team[] | null;
value?: string;
onChange?: (value: string) => void;
}
const TeamDropdown: React.FC<TeamDropdownProps> = ({ teams, value, onChange }) => {
return (
<Select
showSearch
placeholder="Search or select a team"
value={value}
onChange={onChange}
filterOption={(input, option) => {
if (!option) return false;
const teamAlias = option.children?.[0]?.props?.children || '';
return teamAlias.toLowerCase().includes(input.toLowerCase());
}}
optionFilterProp="children"
>
{teams?.map((team) => (
<Select.Option key={team.team_id} value={team.team_id}>
<span className="font-medium">{team.team_alias}</span>{" "}
<span className="text-gray-500">({team.team_id})</span>
</Select.Option>
))}
</Select>
);
};
export default TeamDropdown;
@@ -31,6 +31,7 @@ import {
getGuardrailsList,
} from "./networking";
import { Team } from "./key_team_helpers/key_list";
import TeamDropdown from "./common_components/team_dropdown";
import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
@@ -292,28 +293,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
initialValue={team ? team.team_id : null}
className="mt-8"
>
<Select
showSearch
placeholder="Search or select a team"
onChange={(value) => {
form.setFieldValue('team_id', value);
const selectedTeam = teams?.find(team => team.team_id === value);
setSelectedCreateKeyTeam(selectedTeam || null);
}}
filterOption={(input, option) => {
if (!option) return false;
const optionValue = option.children?.toString() || '';
return optionValue.toLowerCase().includes(input.toLowerCase());
}}
optionFilterProp="children"
>
{teams?.map((team) => (
<Select.Option key={team.team_id} value={team.team_id}>
<span className="font-medium">{team.team_alias}</span>{" "}
<span className="text-gray-500">({team.team_id})</span>
</Select.Option>
))}
</Select>
<TeamDropdown teams={teams} />
</Form.Item>
<Form.Item
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,333 @@
import React, { useState, useEffect } from "react";
import {
Card,
Title,
Text,
Tab,
TabList,
TabGroup,
TabPanel,
TabPanels,
Grid,
Badge,
Button as TremorButton,
} from "@tremor/react";
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
import { modelDeleteCall, modelUpdateCall } from "./networking";
import { Button, Form, Input, InputNumber, message, Select } from "antd";
import EditModelModal from "./edit_model/edit_model_modal";
import { getProviderLogoAndName } from "./provider_info_helpers";
interface ModelInfoViewProps {
modelId: string;
onClose: () => void;
modelData: any;
accessToken: string | null;
userID: string | null;
userRole: string | null;
editModel: boolean;
}
export default function ModelInfoView({
modelId,
onClose,
modelData,
accessToken,
userID,
userRole,
editModel
}: ModelInfoViewProps) {
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const canEditModel = userRole === "Admin";
if (!modelData) {
return (
<div className="p-4">
<Button
icon={ArrowLeftIcon}
onClick={onClose}
className="mb-4"
>
Back to Models
</Button>
<Text>Model not found</Text>
</div>
);
}
const handleDelete = async () => {
try {
if (!accessToken) return;
await modelDeleteCall(accessToken, modelId);
message.success("Model deleted successfully");
onClose();
} catch (error) {
console.error("Error deleting the model:", error);
message.error("Failed to delete model");
}
};
const handleModelUpdate = async (values: any) => {
try {
if (!accessToken) return;
let payload = {
litellm_params: Object.keys(values.litellm_params).length > 0 ? values.litellm_params : undefined,
model_info: values.model_info ? {
id: values.model_info.id,
} : undefined,
};
await modelUpdateCall(accessToken, payload);
message.success("Model updated successfully");
setIsEditing(false);
} catch (error) {
console.error("Error updating model:", error);
message.error("Failed to update model");
}
};
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4"> Back</Button>
<Title>{modelData.model_name}</Title>
<Text className="text-gray-500 font-mono">{modelData.model_info.id}</Text>
</div>
{canEditModel && (
<div className="flex gap-2">
<TremorButton
onClick={() => setIsDeleteModalOpen(true)}
color="red"
>
Delete Model
</TremorButton>
</div>
)}
</div>
<TabGroup>
<TabList className="mb-6">
<Tab>Overview</Tab>
<Tab>Raw JSON</Tab>
</TabList>
<TabPanels>
<TabPanel>
{/* Overview Grid */}
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6 mb-6">
<Card>
<Text>Provider</Text>
<div className="mt-2 flex items-center space-x-2">
{modelData.provider && (
<img
src={getProviderLogoAndName(modelData.provider).logo}
alt={`${modelData.provider} logo`}
className="w-4 h-4"
onError={(e) => {
// Create a div with provider initial as fallback
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs';
fallbackDiv.textContent = modelData.provider?.charAt(0) || '-';
parent.replaceChild(fallbackDiv, target);
}
}}
/>
)}
<Title>{modelData.provider || "Not Set"}</Title>
</div>
</Card>
<Card>
<Text>LiteLLM Model</Text>
<pre>
<Title>{modelData.litellm_model_name || "Not Set"}</Title>
</pre>
</Card>
<Card>
<Text>Pricing</Text>
<div className="mt-2">
<Text>Input: ${modelData.input_cost}/1M tokens</Text>
<Text>Output: ${modelData.output_cost}/1M tokens</Text>
</div>
</Card>
</Grid>
{/* Settings Card */}
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Model Settings</Title>
{(canEditModel && !isEditing) && (
<TremorButton
onClick={() => setIsEditing(true)}
>
Edit Settings
</TremorButton>
)}
</div>
{isEditing ? (
<EditModelModal
visible={isEditing}
onCancel={() => setIsEditing(false)}
model={modelData}
onSubmit={handleModelUpdate}
/>
) : (
<div className="space-y-4">
<div>
<Text className="font-medium">Model ID</Text>
<div className="font-mono">{modelData.model_info.id}</div>
</div>
<div>
<Text className="font-medium">Public Model Name</Text>
<div>{modelData.model_name}</div>
</div>
<div>
<Text className="font-medium">LiteLLM Model Name</Text>
<div>{modelData.litellm_model_name}</div>
</div>
<div>
<Text className="font-medium">Input Cost (per 1M tokens)</Text>
<div>{modelData.litellm_params?.input_cost_per_token ? (modelData.litellm_params.input_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Output Cost (per 1M tokens)</Text>
<div>{modelData.litellm_params?.output_cost_per_token ? (modelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">API Base</Text>
<div>{modelData.litellm_params?.api_base || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Custom LLM Provider</Text>
<div>{modelData.litellm_params?.custom_llm_provider || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Model</Text>
<div>{modelData.litellm_params?.model || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Organization</Text>
<div>{modelData.litellm_params?.organization || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">TPM (Tokens per Minute)</Text>
<div>{modelData.litellm_params?.tpm || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">RPM (Requests per Minute)</Text>
<div>{modelData.litellm_params?.rpm || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Max Retries</Text>
<div>{modelData.litellm_params?.max_retries || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Timeout (seconds)</Text>
<div>{modelData.litellm_params?.timeout || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Stream Timeout (seconds)</Text>
<div>{modelData.litellm_params?.stream_timeout || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Team ID</Text>
<div>{modelData.model_info.team_id || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Created At</Text>
<div>{modelData.model_info.created_at ? new Date(modelData.model_info.created_at).toLocaleString() : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Created By</Text>
<div>{modelData.model_info.created_by || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">LiteLLM Parameters</Text>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(modelData.cleanedLitellmParams, null, 2)}
</pre>
</div>
</div>
)}
</Card>
</TabPanel>
<TabPanel>
<Card>
<pre className="bg-gray-100 p-4 rounded text-xs overflow-auto">
{JSON.stringify(modelData, null, 2)}
</pre>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete Model
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this model?
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button
onClick={handleDelete}
className="ml-2"
danger
>
Delete
</Button>
<Button onClick={() => setIsDeleteModalOpen(false)}>
Cancel
</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
}