mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
UI - Model Hub - refactor 'Make Public' to have a select + confirm form (#12555)
* fix(model_hub_table.tsx): fix null link * fix(model_hub_table.tsx): add tooltip on disabled make public tell user how to enable button * feat(model_hub_table.tsx): make Model Hub OSS allow more teams to share available models via litellm * fix(model_hub_table_columns.tsx): make table view only model hub view, rename 'model' column to 'public model name', make model hub url copyable * fix(page.tsx): fix logo re-rendering * fix(public_model_hub.tsx): fix theme * style(public_model_hub.tsx): move filters into same card as model table filters are for table * fix(page.tsx): fix ui linting error * refactor(model_hub_table.tsx): refactor public model selection to inside the 'make public' button - modal similar flow to adding guardrails consistent flow for doing stuff on the proxy * style(ui/): make 'make public' modal wider, to have all filters on same horizontal axis * style(model_hub_table.tsx): have already public models be pre-selected * fix(model_hub_table.tsx): ensure only admin can make models public via UI * fix: fix linting errors * fix(model_hub_table.tsx): make model hub purpose clearer * fix(model_hub_table_columns.tsx): fix linting error
This commit is contained in:
parent
1f5ab2ee74
commit
d8ae044ec5
6 changed files with 582 additions and 229 deletions
|
|
@ -20,6 +20,6 @@ export default function PublicModelHubTable() {
|
|||
*
|
||||
*/
|
||||
return (
|
||||
<ModelHubTable accessToken={accessToken} publicPage={true} premiumUser={false} />
|
||||
<ModelHubTable accessToken={accessToken} publicPage={true} premiumUser={false} userRole={null}/>
|
||||
);
|
||||
}
|
||||
|
|
@ -375,6 +375,7 @@ export default function CreateKeyPage() {
|
|||
accessToken={accessToken}
|
||||
publicPage={false}
|
||||
premiumUser={premiumUser}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "caching" ? (
|
||||
<CacheDashboard
|
||||
|
|
|
|||
317
ui/litellm-dashboard/src/components/make_model_public_form.tsx
Normal file
317
ui/litellm-dashboard/src/components/make_model_public_form.tsx
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Modal, Form, Steps, Button, message, Checkbox } from "antd";
|
||||
import { Text, Title, Badge } from "@tremor/react";
|
||||
import { makeModelGroupPublic } from "./networking";
|
||||
import ModelFilters from "./model_filters";
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
providers: string[];
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
input_cost_per_token?: number;
|
||||
output_cost_per_token?: number;
|
||||
mode?: string;
|
||||
tpm?: number;
|
||||
rpm?: number;
|
||||
supports_parallel_function_calling: boolean;
|
||||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
is_public_model_group: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface MakeModelPublicFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
accessToken: string;
|
||||
modelHubData: ModelGroupInfo[];
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
accessToken,
|
||||
modelHubData,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
|
||||
const [filteredData, setFilteredData] = useState<ModelGroupInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentStep(0);
|
||||
setSelectedModels(new Set());
|
||||
setFilteredData([]);
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep === 0) {
|
||||
if (selectedModels.size === 0) {
|
||||
message.error("Please select at least one model to make public");
|
||||
return;
|
||||
}
|
||||
setCurrentStep(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep === 1) {
|
||||
setCurrentStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelection = (modelGroup: string, checked: boolean) => {
|
||||
const newSelection = new Set(selectedModels);
|
||||
if (checked) {
|
||||
newSelection.add(modelGroup);
|
||||
} else {
|
||||
newSelection.delete(modelGroup);
|
||||
}
|
||||
setSelectedModels(newSelection);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allModelGroups = filteredData.map(model => model.model_group);
|
||||
setSelectedModels(new Set(allModelGroups));
|
||||
} else {
|
||||
setSelectedModels(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilteredDataChange = useCallback((newFilteredData: ModelGroupInfo[]) => {
|
||||
setFilteredData(newFilteredData);
|
||||
// Keep existing selections when filters change - don't clear them
|
||||
}, []);
|
||||
|
||||
// Initialize filtered data and preselect already public models when modal opens
|
||||
useEffect(() => {
|
||||
if (visible && modelHubData.length > 0) {
|
||||
setFilteredData(modelHubData);
|
||||
|
||||
// Preselect models that are already public
|
||||
const alreadyPublicModels = modelHubData
|
||||
.filter(model => model.is_public_model_group === true)
|
||||
.map(model => model.model_group);
|
||||
|
||||
setSelectedModels(new Set(alreadyPublicModels));
|
||||
}
|
||||
}, [visible, modelHubData]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (selectedModels.size === 0) {
|
||||
message.error("Please select at least one model to make public");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const modelGroupsToMakePublic = Array.from(selectedModels);
|
||||
await makeModelGroupPublic(accessToken, modelGroupsToMakePublic);
|
||||
|
||||
message.success(`Successfully made ${modelGroupsToMakePublic.length} model group(s) public!`);
|
||||
handleClose();
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Error making model groups public:", error);
|
||||
message.error("Failed to make model groups public. Please try again.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStep1Content = () => {
|
||||
const allModelsSelected = filteredData.length > 0 && filteredData.every(model => selectedModels.has(model.model_group));
|
||||
const isIndeterminate = selectedModels.size > 0 && !allModelsSelected;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title>Select Models to Make Public</Title>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={allModelsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
disabled={filteredData.length === 0}
|
||||
>
|
||||
Select All {filteredData.length > 0 && `(${filteredData.length})`}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-gray-600">
|
||||
Select the models you want to make publicly accessible. These models will be visible to anyone with access to the public model hub.
|
||||
</Text>
|
||||
|
||||
{/* Filters */}
|
||||
<ModelFilters
|
||||
modelHubData={modelHubData}
|
||||
onFilteredDataChange={handleFilteredDataChange}
|
||||
showFiltersCard={false}
|
||||
className="border rounded-lg p-4 bg-gray-50"
|
||||
/>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
{filteredData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Text>No models match the current filters.</Text>
|
||||
</div>
|
||||
) : (
|
||||
filteredData.map((model) => (
|
||||
<div key={model.model_group} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50">
|
||||
<Checkbox
|
||||
checked={selectedModels.has(model.model_group)}
|
||||
onChange={(e) => handleModelSelection(model.model_group, e.target.checked)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{model.model_group}</Text>
|
||||
{model.mode && (
|
||||
<Badge color="green" size="sm">{model.mode}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{model.providers.map(provider => (
|
||||
<Badge key={provider} color="blue" size="xs">{provider}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedModels.size > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<strong>{selectedModels.size}</strong> model{selectedModels.size !== 1 ? 's' : ''} selected
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep2Content = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Title>Confirm Making Models Public</Title>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> Once you make these models public, anyone who can go to the <code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Text className="font-medium">Models to be made public:</Text>
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{Array.from(selectedModels).map((modelGroup) => {
|
||||
const model = modelHubData.find(m => m.model_group === modelGroup);
|
||||
return (
|
||||
<div key={modelGroup} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<div>
|
||||
<Text className="font-medium">{modelGroup}</Text>
|
||||
{model && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{model.providers.map(provider => (
|
||||
<Badge key={provider} color="blue" size="xs">{provider}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
Total: <strong>{selectedModels.size}</strong> model{selectedModels.size !== 1 ? 's' : ''} will be made public
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
return renderStep1Content();
|
||||
case 1:
|
||||
return renderStep2Content();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepButtons = () => {
|
||||
return (
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button
|
||||
onClick={currentStep === 0 ? handleClose : handlePrevious}
|
||||
>
|
||||
{currentStep === 0 ? "Cancel" : "Previous"}
|
||||
</Button>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={selectedModels.size === 0}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
>
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Make Models Public"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Steps current={currentStep} className="mb-6">
|
||||
<Step title="Select Models" />
|
||||
<Step title="Confirm" />
|
||||
</Steps>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakeModelPublicForm;
|
||||
212
ui/litellm-dashboard/src/components/model_filters.tsx
Normal file
212
ui/litellm-dashboard/src/components/model_filters.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
providers: string[];
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
input_cost_per_token?: number;
|
||||
output_cost_per_token?: number;
|
||||
mode?: string;
|
||||
tpm?: number;
|
||||
rpm?: number;
|
||||
supports_parallel_function_calling: boolean;
|
||||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
is_public_model_group: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ModelFiltersProps {
|
||||
modelHubData: ModelGroupInfo[];
|
||||
onFilteredDataChange: (filteredData: ModelGroupInfo[]) => void;
|
||||
showFiltersCard?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ModelFilters: React.FC<ModelFiltersProps> = ({
|
||||
modelHubData,
|
||||
onFilteredDataChange,
|
||||
showFiltersCard = true,
|
||||
className = "",
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("");
|
||||
const [selectedMode, setSelectedMode] = useState<string>("");
|
||||
const [selectedFeature, setSelectedFeature] = useState<string>("");
|
||||
const previousFilteredDataRef = useRef<ModelGroupInfo[]>([]);
|
||||
|
||||
// Helper functions to get unique values
|
||||
const getUniqueProviders = (data: ModelGroupInfo[]) => {
|
||||
const providers = new Set<string>();
|
||||
data.forEach(model => {
|
||||
model.providers.forEach(provider => providers.add(provider));
|
||||
});
|
||||
return Array.from(providers);
|
||||
};
|
||||
|
||||
const getUniqueModes = (data: ModelGroupInfo[]) => {
|
||||
const modes = new Set<string>();
|
||||
data.forEach(model => {
|
||||
if (model.mode) modes.add(model.mode);
|
||||
});
|
||||
return Array.from(modes);
|
||||
};
|
||||
|
||||
const getUniqueFeatures = (data: ModelGroupInfo[]) => {
|
||||
const features = new Set<string>();
|
||||
data.forEach(model => {
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.forEach(([key]) => {
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
features.add(featureName);
|
||||
});
|
||||
});
|
||||
return Array.from(features).sort();
|
||||
};
|
||||
|
||||
// Memoized filtered data
|
||||
const filteredData = useMemo(() => {
|
||||
return modelHubData?.filter(model => {
|
||||
const matchesSearch = model.model_group.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesProvider = selectedProvider === "" || model.providers.includes(selectedProvider);
|
||||
const matchesMode = selectedMode === "" || model.mode === selectedMode;
|
||||
|
||||
// Check if model has the selected feature
|
||||
const matchesFeature = selectedFeature === "" ||
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.some(([key]) => {
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return featureName === selectedFeature;
|
||||
});
|
||||
|
||||
return matchesSearch && matchesProvider && matchesMode && matchesFeature;
|
||||
}) || [];
|
||||
}, [modelHubData, searchTerm, selectedProvider, selectedMode, selectedFeature]);
|
||||
|
||||
// Update parent component when filtered data changes
|
||||
useEffect(() => {
|
||||
// Only call the callback if the filtered data actually changed
|
||||
const hasChanged =
|
||||
filteredData.length !== previousFilteredDataRef.current.length ||
|
||||
filteredData.some((model, index) =>
|
||||
model.model_group !== previousFilteredDataRef.current[index]?.model_group
|
||||
);
|
||||
|
||||
if (hasChanged) {
|
||||
previousFilteredDataRef.current = filteredData;
|
||||
onFilteredDataChange(filteredData);
|
||||
}
|
||||
}, [filteredData, onFilteredDataChange]);
|
||||
|
||||
// Reset filters function
|
||||
const resetFilters = () => {
|
||||
setSearchTerm("");
|
||||
setSelectedProvider("");
|
||||
setSelectedMode("");
|
||||
setSelectedFeature("");
|
||||
};
|
||||
|
||||
// Expose filter values and reset function
|
||||
const filterValues = {
|
||||
searchTerm,
|
||||
selectedProvider,
|
||||
selectedMode,
|
||||
selectedFeature,
|
||||
resetFilters,
|
||||
};
|
||||
|
||||
const filtersContent = (
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Search Models:</Text>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search model names..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border rounded px-3 py-2 w-64 h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Provider:</Text>
|
||||
<select
|
||||
value={selectedProvider}
|
||||
onChange={(e) => setSelectedProvider(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-40 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Providers</option>
|
||||
{modelHubData && getUniqueProviders(modelHubData).map(provider => (
|
||||
<option key={provider} value={provider} className="text-sm text-gray-800">{provider}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Mode:</Text>
|
||||
<select
|
||||
value={selectedMode}
|
||||
onChange={(e) => setSelectedMode(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-32 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Modes</option>
|
||||
{modelHubData && getUniqueModes(modelHubData).map(mode => (
|
||||
<option key={mode} value={mode} className="text-sm text-gray-800">{mode}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Features:</Text>
|
||||
<select
|
||||
value={selectedFeature}
|
||||
onChange={(e) => setSelectedFeature(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-48 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Features</option>
|
||||
{modelHubData && getUniqueFeatures(modelHubData).map(feature => (
|
||||
<option key={feature} value={feature} className="text-sm text-gray-800">{feature}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Clear filters button */}
|
||||
{(searchTerm || selectedProvider || selectedMode || selectedFeature) && (
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm underline h-10 flex items-center"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (showFiltersCard) {
|
||||
return (
|
||||
<Card className={`mb-6 ${className}`}>
|
||||
{filtersContent}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{filtersContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelFilters;
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { modelHubCall, makeModelGroupPublic, modelHubPublicModelsCall, getProxyBaseUrl } from "./networking";
|
||||
import { getConfigFieldSetting, updateConfigFieldSetting } from "./networking";
|
||||
import { ModelDataTable } from "./model_dashboard/table";
|
||||
import { modelHubColumns } from "./model_hub_table_columns";
|
||||
import PublicModelHub from "./public_model_hub";
|
||||
import MakeModelPublicForm from "./make_model_public_form";
|
||||
import ModelFilters from "./model_filters";
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
|
|
@ -17,11 +19,13 @@ import { Modal, message, Tooltip } from "antd";
|
|||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { Table as TableInstance } from '@tanstack/react-table';
|
||||
import { Copy } from "lucide-react";
|
||||
import { isAdminRole } from "../utils/roles";
|
||||
|
||||
interface ModelHubTableProps {
|
||||
accessToken: string | null;
|
||||
publicPage: boolean;
|
||||
premiumUser: boolean;
|
||||
userRole: string | null;
|
||||
}
|
||||
|
||||
interface ModelGroupInfo {
|
||||
|
|
@ -38,7 +42,7 @@ interface ModelGroupInfo {
|
|||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
public?: boolean; // Whether the model is public (defaults to false)
|
||||
is_public_model_group: boolean;
|
||||
// Allow any additional properties for flexibility
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
@ -47,6 +51,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
accessToken,
|
||||
publicPage,
|
||||
premiumUser,
|
||||
userRole,
|
||||
}) => {
|
||||
const [publicPageAllowed, setPublicPageAllowed] = useState<boolean>(false);
|
||||
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
|
||||
|
|
@ -54,11 +59,8 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isPublicPageModalVisible, setIsPublicPageModalVisible] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("");
|
||||
const [selectedMode, setSelectedMode] = useState<string>("");
|
||||
const [selectedFeature, setSelectedFeature] = useState<string>("");
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
|
||||
const [filteredData, setFilteredData] = useState<ModelGroupInfo[]>([]);
|
||||
const [isMakePublicModalVisible, setIsMakePublicModalVisible] = useState(false);
|
||||
const router = useRouter();
|
||||
const tableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
|
|
@ -120,34 +122,13 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
router.replace(`/model_hub_table?key=${accessToken}`);
|
||||
};
|
||||
|
||||
const handleMakePublicPage = async () => {
|
||||
const handleMakePublicPage = () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the selected model groups or use all if none are selected
|
||||
const modelGroupsToMakePublic = selectedModels.size > 0
|
||||
? Array.from(selectedModels)
|
||||
: modelHubData?.map(model => model.model_group) || [];
|
||||
|
||||
if (modelGroupsToMakePublic.length > 0) {
|
||||
// Call the endpoint to make the selected model groups public
|
||||
await makeModelGroupPublic(accessToken, modelGroupsToMakePublic);
|
||||
|
||||
// Show success message
|
||||
message.success(`Successfully made ${modelGroupsToMakePublic.length} model group(s) public!`);
|
||||
|
||||
// Route to the model hub table
|
||||
router.push(`/ui/model_hub_table`);
|
||||
} else {
|
||||
// Show the modal if no model groups available
|
||||
setIsPublicPageModalVisible(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error making model groups public:", error);
|
||||
message.error("Failed to make model groups public. Please try again.");
|
||||
}
|
||||
// Show the modal for selecting models to make public
|
||||
setIsMakePublicModalVisible(true);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
|
|
@ -187,90 +168,24 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
return `$${(cost * 1_000_000).toFixed(2)}`;
|
||||
};
|
||||
|
||||
const getUniqueProviders = (data: ModelGroupInfo[]) => {
|
||||
const providers = new Set<string>();
|
||||
data.forEach(model => {
|
||||
model.providers.forEach(provider => providers.add(provider));
|
||||
});
|
||||
return Array.from(providers);
|
||||
};
|
||||
|
||||
const getUniqueModes = (data: ModelGroupInfo[]) => {
|
||||
const modes = new Set<string>();
|
||||
data.forEach(model => {
|
||||
if (model.mode) modes.add(model.mode);
|
||||
});
|
||||
return Array.from(modes);
|
||||
};
|
||||
|
||||
const getUniqueFeatures = (data: ModelGroupInfo[]) => {
|
||||
const features = new Set<string>();
|
||||
data.forEach(model => {
|
||||
// Find all properties that start with 'supports_' and are true
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.forEach(([key]) => {
|
||||
// Format the feature name (remove 'supports_' prefix and convert to title case)
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
features.add(featureName);
|
||||
});
|
||||
});
|
||||
return Array.from(features).sort();
|
||||
};
|
||||
|
||||
const filteredData = modelHubData?.filter(model => {
|
||||
const matchesSearch = model.model_group.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesProvider = selectedProvider === "" || model.providers.includes(selectedProvider);
|
||||
const matchesMode = selectedMode === "" || model.mode === selectedMode;
|
||||
|
||||
// Check if model has the selected feature
|
||||
const matchesFeature = selectedFeature === "" ||
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.some(([key]) => {
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return featureName === selectedFeature;
|
||||
});
|
||||
|
||||
return matchesSearch && matchesProvider && matchesMode && matchesFeature;
|
||||
}) || [];
|
||||
|
||||
const handleRowSelection = (modelGroup: string, isSelected: boolean) => {
|
||||
const newSelection = new Set(selectedModels);
|
||||
if (isSelected) {
|
||||
newSelection.add(modelGroup);
|
||||
} else {
|
||||
newSelection.delete(modelGroup);
|
||||
}
|
||||
setSelectedModels(newSelection);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
console.log("checked", checked);
|
||||
if (checked) {
|
||||
const allModelGroups = filteredData.map(model => model.model_group);
|
||||
setSelectedModels(new Set(allModelGroups));
|
||||
} else {
|
||||
setSelectedModels(new Set());
|
||||
const handleMakePublicSuccess = () => {
|
||||
// Refresh the model hub data after successful public operation
|
||||
if (accessToken) {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const _modelHubData = await modelHubCall(accessToken);
|
||||
setModelHubData(_modelHubData.data);
|
||||
} catch (error) {
|
||||
console.error("Error refreshing model data:", error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
// Use the same logic as health check columns
|
||||
const allModelsSelected = filteredData.length > 0 && filteredData.every(model => selectedModels.has(model.model_group));
|
||||
const isIndeterminate = selectedModels.size > 0 && !allModelsSelected;
|
||||
|
||||
// Clear selections when filters change to avoid confusion
|
||||
useEffect(() => {
|
||||
setSelectedModels(new Set());
|
||||
}, [searchTerm, selectedProvider, selectedMode, selectedFeature]);
|
||||
const handleFilteredDataChange = useCallback((newFilteredData: ModelGroupInfo[]) => {
|
||||
setFilteredData(newFilteredData);
|
||||
}, []);
|
||||
|
||||
console.log("publicPage: ", publicPage);
|
||||
console.log("publicPageAllowed: ", publicPageAllowed);
|
||||
|
|
@ -287,9 +202,15 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<Title className="text-center">Model Hub</Title>
|
||||
{isAdminRole(userRole || "") ? (
|
||||
<p className="text-sm text-gray-600">
|
||||
Make models public for developers to know what models are available on the proxy.
|
||||
</p>
|
||||
): (
|
||||
<p className="text-sm text-gray-600">
|
||||
A list of all public model names personally available to you.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Text>Model Hub URL:</Text>
|
||||
|
|
@ -304,86 +225,26 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{publicPage == false && (
|
||||
<Tooltip
|
||||
title={selectedModels.size === 0 ? "Select models to make them publicly known" : ""}
|
||||
placement="top"
|
||||
>
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
<Button
|
||||
className="ml-4"
|
||||
onClick={() => handleMakePublicPage()}
|
||||
disabled={selectedModels.size === 0}
|
||||
>
|
||||
Make Public
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Search Models:</Text>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search model names..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border rounded px-3 py-2 w-64 h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Provider:</Text>
|
||||
<select
|
||||
value={selectedProvider}
|
||||
onChange={(e) => setSelectedProvider(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-40 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Providers</option>
|
||||
{modelHubData && getUniqueProviders(modelHubData).map(provider => (
|
||||
<option key={provider} value={provider} className="text-sm text-gray-800">{provider}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Mode:</Text>
|
||||
<select
|
||||
value={selectedMode}
|
||||
onChange={(e) => setSelectedMode(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-32 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Modes</option>
|
||||
{modelHubData && getUniqueModes(modelHubData).map(mode => (
|
||||
<option key={mode} value={mode} className="text-sm text-gray-800">{mode}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Features:</Text>
|
||||
<select
|
||||
value={selectedFeature}
|
||||
onChange={(e) => setSelectedFeature(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-48 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Features</option>
|
||||
{modelHubData && getUniqueFeatures(modelHubData).map(feature => (
|
||||
<option key={feature} value={feature} className="text-sm text-gray-800">{feature}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<ModelFilters
|
||||
modelHubData={modelHubData || []}
|
||||
onFilteredDataChange={handleFilteredDataChange}
|
||||
/>
|
||||
|
||||
{/* Model Table */}
|
||||
<ModelDataTable
|
||||
columns={modelHubColumns(
|
||||
selectedModels,
|
||||
allModelsSelected,
|
||||
isIndeterminate,
|
||||
handleRowSelection,
|
||||
handleSelectAll,
|
||||
showModal,
|
||||
copyToClipboard,
|
||||
publicPage,
|
||||
|
|
@ -398,21 +259,6 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
{selectedModels.size > 0 && (
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Text className="text-sm text-blue-600">
|
||||
{selectedModels.size} model{selectedModels.size !== 1 ? 's' : ''} selected
|
||||
</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
onClick={() => setSelectedModels(new Set())}
|
||||
className="text-xs"
|
||||
>
|
||||
Unselect All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -589,6 +435,15 @@ print(response.choices[0].message.content)`}
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Make Model Public Form */}
|
||||
<MakeModelPublicForm
|
||||
visible={isMakePublicModalVisible}
|
||||
onClose={() => setIsMakePublicModalVisible(false)}
|
||||
accessToken={accessToken || ""}
|
||||
modelHubData={modelHubData || []}
|
||||
onSuccess={handleMakePublicSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Badge, Text } from "@tremor/react";
|
||||
import { Tooltip, Checkbox, Tag } from "antd";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import {
|
||||
CopyOutlined,
|
||||
InfoCircleOutlined
|
||||
|
|
@ -20,7 +20,7 @@ interface ModelHubData {
|
|||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
public?: boolean;
|
||||
is_public_model_group: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
@ -52,40 +52,11 @@ const formatTokens = (tokens: number) => {
|
|||
};
|
||||
|
||||
export const modelHubColumns = (
|
||||
selectedModels: Set<string>,
|
||||
allModelsSelected: boolean,
|
||||
isIndeterminate: boolean,
|
||||
handleModelSelection: (modelGroup: string, checked: boolean) => void,
|
||||
handleSelectAll: (checked: boolean) => void,
|
||||
showModal: (model: ModelHubData) => void,
|
||||
copyToClipboard: (text: string) => void,
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<ModelHubData>[] => {
|
||||
const allColumns: ColumnDef<ModelHubData>[] = [
|
||||
{
|
||||
header: () => (
|
||||
<Checkbox
|
||||
checked={allModelsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
),
|
||||
id: "select",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const isSelected = selectedModels.has(model.model_group);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => handleModelSelection(model.model_group, e.target.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Public Model Name",
|
||||
accessorKey: "model_group",
|
||||
|
|
@ -240,7 +211,7 @@ export const modelHubColumns = (
|
|||
},
|
||||
{
|
||||
header: "Public",
|
||||
accessorKey: "public",
|
||||
accessorKey: "is_public_model_group",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const publicA = rowA.original.is_public_model_group === true ? 1 : 0;
|
||||
|
|
@ -285,11 +256,8 @@ export const modelHubColumns = (
|
|||
// Filter out columns based on publicPage setting
|
||||
if (publicPage) {
|
||||
return allColumns.filter(column => {
|
||||
// Remove the select/checkbox column
|
||||
if (column.id === "select") return false;
|
||||
|
||||
// Remove the public column
|
||||
if ('accessorKey' in column && column.accessorKey === "public") return false;
|
||||
if ('accessorKey' in column && column.accessorKey === "is_public_model_group") return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue