Spaces:
Running
Running
import React, { useState, useEffect } from "react"; | |
import { | |
Card, | |
Title, | |
Text, | |
Tab, | |
TabList, | |
TabGroup, | |
TabPanel, | |
TabPanels, | |
Grid, | |
Badge, | |
Button as TremorButton, | |
TextInput, | |
} from "@tremor/react"; | |
import NumericalInput from "./shared/numerical_input"; | |
import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline"; | |
import { modelDeleteCall, modelUpdateCall, CredentialItem, credentialGetCall, credentialCreateCall, modelInfoCall, modelInfoV1Call, modelPatchUpdateCall } from "./networking"; | |
import { Button, Form, Input, InputNumber, message, Select, Modal } from "antd"; | |
import EditModelModal from "./edit_model/edit_model_modal"; | |
import { handleEditModelSubmit } from "./edit_model/edit_model_modal"; | |
import { getProviderLogoAndName } from "./provider_info_helpers"; | |
import { getDisplayModelName } from "./view_model/model_name_display"; | |
import AddCredentialsModal from "./model_add/add_credentials_tab"; | |
import ReuseCredentialsModal from "./model_add/reuse_credentials"; | |
import CacheControlSettings from "./add_model/cache_control_settings"; | |
interface ModelInfoViewProps { | |
modelId: string; | |
onClose: () => void; | |
modelData: any; | |
accessToken: string | null; | |
userID: string | null; | |
userRole: string | null; | |
editModel: boolean; | |
setEditModalVisible: (visible: boolean) => void; | |
setSelectedModel: (model: any) => void; | |
onModelUpdate?: (updatedModel: any) => void; | |
} | |
export default function ModelInfoView({ | |
modelId, | |
onClose, | |
modelData, | |
accessToken, | |
userID, | |
userRole, | |
editModel, | |
setEditModalVisible, | |
setSelectedModel, | |
onModelUpdate | |
}: ModelInfoViewProps) { | |
const [form] = Form.useForm(); | |
const [localModelData, setLocalModelData] = useState<any>(null); | |
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | |
const [isCredentialModalOpen, setIsCredentialModalOpen] = useState(false); | |
const [isDirty, setIsDirty] = useState(false); | |
const [isSaving, setIsSaving] = useState(false); | |
const [isEditing, setIsEditing] = useState(false); | |
const [existingCredential, setExistingCredential] = useState<CredentialItem | null>(null); | |
const [showCacheControl, setShowCacheControl] = useState(false); | |
const canEditModel = userRole === "Admin" || modelData.model_info.created_by === userID; | |
const isAdmin = userRole === "Admin"; | |
const usingExistingCredential = modelData.litellm_params?.litellm_credential_name != null && modelData.litellm_params?.litellm_credential_name != undefined; | |
console.log("usingExistingCredential, ", usingExistingCredential); | |
console.log("modelData.litellm_params.litellm_credential_name, ", modelData.litellm_params.litellm_credential_name); | |
useEffect(() => { | |
const getExistingCredential = async () => { | |
console.log("accessToken, ", accessToken); | |
if (!accessToken) return; | |
if (usingExistingCredential) return; | |
let existingCredentialResponse = await credentialGetCall(accessToken, null, modelId); | |
console.log("existingCredentialResponse, ", existingCredentialResponse); | |
setExistingCredential({ | |
credential_name: existingCredentialResponse["credential_name"], | |
credential_values: existingCredentialResponse["credential_values"], | |
credential_info: existingCredentialResponse["credential_info"] | |
}); | |
} | |
const getModelInfo = async () => { | |
if (!accessToken) return; | |
let modelInfoResponse = await modelInfoV1Call(accessToken, modelId); | |
console.log("modelInfoResponse, ", modelInfoResponse); | |
let specificModelData = modelInfoResponse.data[0]; | |
setLocalModelData(specificModelData); | |
// Check if cache control is enabled | |
if (specificModelData?.litellm_params?.cache_control_injection_points) { | |
setShowCacheControl(true); | |
} | |
} | |
getExistingCredential(); | |
getModelInfo(); | |
}, [accessToken, modelId]); | |
const handleReuseCredential = async (values: any) => { | |
console.log("values, ", values); | |
if (!accessToken) return; | |
let credentialItem = { | |
credential_name: values.credential_name, | |
model_id: modelId, | |
credential_info: { | |
"custom_llm_provider": localModelData.litellm_params?.custom_llm_provider, | |
} | |
} | |
message.info("Storing credential.."); | |
let credentialResponse = await credentialCreateCall(accessToken, credentialItem); | |
console.log("credentialResponse, ", credentialResponse); | |
message.success("Credential stored successfully"); | |
} | |
const handleModelUpdate = async (values: any) => { | |
try { | |
if (!accessToken) return; | |
setIsSaving(true); | |
console.log("values.model_name, ", values.model_name); | |
let updatedLitellmParams = { | |
...localModelData.litellm_params, | |
model: values.litellm_model_name, | |
api_base: values.api_base, | |
custom_llm_provider: values.custom_llm_provider, | |
organization: values.organization, | |
tpm: values.tpm, | |
rpm: values.rpm, | |
max_retries: values.max_retries, | |
timeout: values.timeout, | |
stream_timeout: values.stream_timeout, | |
input_cost_per_token: values.input_cost / 1_000_000, | |
output_cost_per_token: values.output_cost / 1_000_000, | |
}; | |
// Handle cache control settings | |
if (values.cache_control && values.cache_control_injection_points?.length > 0) { | |
updatedLitellmParams.cache_control_injection_points = values.cache_control_injection_points; | |
} else { | |
delete updatedLitellmParams.cache_control_injection_points; | |
} | |
const updateData = { | |
model_name: values.model_name, | |
litellm_params: updatedLitellmParams, | |
model_info: { | |
id: modelId, | |
} | |
}; | |
await modelPatchUpdateCall(accessToken, updateData, modelId); | |
const updatedModelData = { | |
...localModelData, | |
model_name: values.model_name, | |
litellm_model_name: values.litellm_model_name, | |
litellm_params: updatedLitellmParams | |
}; | |
setLocalModelData(updatedModelData); | |
if (onModelUpdate) { | |
onModelUpdate(updatedModelData); | |
} | |
message.success("Model settings updated successfully"); | |
setIsDirty(false); | |
setIsEditing(false); | |
} catch (error) { | |
console.error("Error updating model:", error); | |
message.error("Failed to update model settings"); | |
} finally { | |
setIsSaving(false); | |
} | |
}; | |
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"); | |
if (onModelUpdate) { | |
onModelUpdate({ | |
deleted: true, | |
model_info: { id: modelId } | |
}); | |
} | |
onClose(); | |
} catch (error) { | |
console.error("Error deleting the model:", error); | |
message.error("Failed to delete model"); | |
} | |
}; | |
return ( | |
<div className="p-4"> | |
<div className="flex justify-between items-center mb-6"> | |
<div> | |
<Button | |
icon={<ArrowLeftIcon />} | |
onClick={onClose} | |
className="mb-4" | |
> | |
Back to Models | |
</Button> | |
<Title>Public Model Name: {getDisplayModelName(modelData)}</Title> | |
<Text className="text-gray-500 font-mono">{modelData.model_info.id}</Text> | |
</div> | |
<div className="flex gap-2"> | |
{isAdmin && ( | |
<TremorButton | |
icon={KeyIcon} | |
variant="secondary" | |
onClick={() => setIsCredentialModalOpen(true)} | |
className="flex items-center" | |
> | |
Re-use Credentials | |
</TremorButton> | |
)} | |
{canEditModel && ( | |
<TremorButton | |
icon={TrashIcon} | |
variant="secondary" | |
onClick={() => setIsDeleteModalOpen(true)} | |
className="flex items-center" | |
> | |
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> | |
{/* Audit info shown as a subtle banner below the overview */} | |
<div className="mb-6 text-sm text-gray-500 flex items-center gap-x-6"> | |
<div className="flex items-center gap-x-2"> | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
Created At {modelData.model_info.created_at | |
? new Date(modelData.model_info.created_at).toLocaleDateString('en-US', { | |
month: 'short', | |
day: 'numeric', | |
year: 'numeric' | |
}) | |
: "Not Set"} | |
</div> | |
<div className="flex items-center gap-x-2"> | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> | |
</svg> | |
Created By {modelData.model_info.created_by || "Not Set"} | |
</div> | |
</div> | |
{/* Settings Card */} | |
<Card> | |
<div className="flex justify-between items-center mb-4"> | |
<Title>Model Settings</Title> | |
{canEditModel && !isEditing && ( | |
<TremorButton | |
variant="secondary" | |
onClick={() => setIsEditing(true)} | |
className="flex items-center" | |
> | |
Edit Model | |
</TremorButton> | |
)} | |
</div> | |
{localModelData ? ( | |
<Form | |
form={form} | |
onFinish={handleModelUpdate} | |
initialValues={{ | |
model_name: localModelData.model_name, | |
litellm_model_name: localModelData.litellm_model_name, | |
api_base: localModelData.litellm_params.api_base, | |
custom_llm_provider: localModelData.litellm_params.custom_llm_provider, | |
organization: localModelData.litellm_params.organization, | |
tpm: localModelData.litellm_params.tpm, | |
rpm: localModelData.litellm_params.rpm, | |
max_retries: localModelData.litellm_params.max_retries, | |
timeout: localModelData.litellm_params.timeout, | |
stream_timeout: localModelData.litellm_params.stream_timeout, | |
input_cost: localModelData.litellm_params.input_cost_per_token ? | |
(localModelData.litellm_params.input_cost_per_token * 1_000_000) : localModelData.model_info?.input_cost_per_token * 1_000_000 || null, | |
output_cost: localModelData.litellm_params?.output_cost_per_token ? | |
(localModelData.litellm_params.output_cost_per_token * 1_000_000) : localModelData.model_info?.output_cost_per_token * 1_000_000 || null, | |
cache_control: localModelData.litellm_params?.cache_control_injection_points ? true : false, | |
cache_control_injection_points: localModelData.litellm_params?.cache_control_injection_points || [], | |
}} | |
layout="vertical" | |
onValuesChange={() => setIsDirty(true)} | |
> | |
<div className="space-y-4"> | |
<div className="space-y-4"> | |
<div> | |
<Text className="font-medium">Model Name</Text> | |
{isEditing ? ( | |
<Form.Item name="model_name" className="mb-0"> | |
<TextInput placeholder="Enter model name" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded">{localModelData.model_name}</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">LiteLLM Model Name</Text> | |
{isEditing ? ( | |
<Form.Item name="litellm_model_name" className="mb-0"> | |
<TextInput placeholder="Enter LiteLLM model name" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded">{localModelData.litellm_model_name}</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Input Cost (per 1M tokens)</Text> | |
{isEditing ? ( | |
<Form.Item name="input_cost" className="mb-0"> | |
<NumericalInput placeholder="Enter input cost" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData?.litellm_params?.input_cost_per_token | |
? (localModelData.litellm_params?.input_cost_per_token * 1_000_000).toFixed(4) | |
: localModelData?.model_info?.input_cost_per_token ? (localModelData.model_info.input_cost_per_token * 1_000_000).toFixed(4) : null} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Output Cost (per 1M tokens)</Text> | |
{isEditing ? ( | |
<Form.Item name="output_cost" className="mb-0"> | |
<NumericalInput placeholder="Enter output cost" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData?.litellm_params?.output_cost_per_token | |
? (localModelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) | |
: localModelData?.model_info?.output_cost_per_token ? (localModelData.model_info.output_cost_per_token * 1_000_000).toFixed(4) : null} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">API Base</Text> | |
{isEditing ? ( | |
<Form.Item name="api_base" className="mb-0"> | |
<TextInput placeholder="Enter API base" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.api_base || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Custom LLM Provider</Text> | |
{isEditing ? ( | |
<Form.Item name="custom_llm_provider" className="mb-0"> | |
<TextInput placeholder="Enter custom LLM provider" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.custom_llm_provider || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Organization</Text> | |
{isEditing ? ( | |
<Form.Item name="organization" className="mb-0"> | |
<TextInput placeholder="Enter organization" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.organization || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">TPM (Tokens per Minute)</Text> | |
{isEditing ? ( | |
<Form.Item name="tpm" className="mb-0"> | |
<NumericalInput placeholder="Enter TPM" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.tpm || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">RPM (Requests per Minute)</Text> | |
{isEditing ? ( | |
<Form.Item name="rpm" className="mb-0"> | |
<NumericalInput placeholder="Enter RPM" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.rpm || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Max Retries</Text> | |
{isEditing ? ( | |
<Form.Item name="max_retries" className="mb-0"> | |
<NumericalInput placeholder="Enter max retries" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.max_retries || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Timeout (seconds)</Text> | |
{isEditing ? ( | |
<Form.Item name="timeout" className="mb-0"> | |
<NumericalInput placeholder="Enter timeout" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.timeout || "Not Set"} | |
</div> | |
)} | |
</div> | |
<div> | |
<Text className="font-medium">Stream Timeout (seconds)</Text> | |
{isEditing ? ( | |
<Form.Item name="stream_timeout" className="mb-0"> | |
<NumericalInput placeholder="Enter stream timeout" /> | |
</Form.Item> | |
) : ( | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.stream_timeout || "Not Set"} | |
</div> | |
)} | |
</div> | |
{/* Cache Control Section */} | |
{isEditing ? ( | |
<CacheControlSettings | |
form={form} | |
showCacheControl={showCacheControl} | |
onCacheControlChange={(checked) => setShowCacheControl(checked)} | |
/> | |
) : ( | |
<div> | |
<Text className="font-medium">Cache Control</Text> | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{localModelData.litellm_params?.cache_control_injection_points ? ( | |
<div> | |
<p>Enabled</p> | |
<div className="mt-2"> | |
{localModelData.litellm_params.cache_control_injection_points.map((point: any, i: number) => ( | |
<div key={i} className="text-sm text-gray-600 mb-1"> | |
Location: {point.location}, | |
{point.role && <span> Role: {point.role}</span>} | |
{point.index !== undefined && <span> Index: {point.index}</span>} | |
</div> | |
))} | |
</div> | |
</div> | |
) : ( | |
"Disabled" | |
)} | |
</div> | |
</div> | |
)} | |
<div> | |
<Text className="font-medium">Team ID</Text> | |
<div className="mt-1 p-2 bg-gray-50 rounded"> | |
{modelData.model_info.team_id || "Not Set"} | |
</div> | |
</div> | |
</div> | |
{isEditing && ( | |
<div className="mt-6 flex justify-end gap-2"> | |
<TremorButton | |
variant="secondary" | |
onClick={() => { | |
form.resetFields(); | |
setIsDirty(false); | |
setIsEditing(false); | |
}} | |
> | |
Cancel | |
</TremorButton> | |
<TremorButton | |
variant="primary" | |
onClick={() => form.submit()} | |
loading={isSaving} | |
> | |
Save Changes | |
</TremorButton> | |
</div> | |
)} | |
</div> | |
</Form> | |
) : ( | |
<Text>Loading...</Text> | |
)} | |
</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">​</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> | |
)} | |
{isCredentialModalOpen && | |
!usingExistingCredential ? ( | |
<ReuseCredentialsModal | |
isVisible={isCredentialModalOpen} | |
onCancel={() => setIsCredentialModalOpen(false)} | |
onAddCredential={handleReuseCredential} | |
existingCredential={existingCredential} | |
setIsCredentialModalOpen={setIsCredentialModalOpen} | |
/> | |
): ( | |
<Modal | |
open={isCredentialModalOpen} | |
onCancel={() => setIsCredentialModalOpen(false)} | |
title="Using Existing Credential" | |
> | |
<Text>{modelData.litellm_params.litellm_credential_name}</Text> | |
</Modal> | |
)} | |
</div> | |
); | |
} |