import { IconLoader2, IconPlus, IconStar } from "@tabler/icons-react" import { useCallback, useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { type ModelInfo, type ModelProviderOption, getModels, setDefaultModel, } from "@/api/models" import { PageHeader } from "@/components/page-header" import { Button } from "@/components/ui/button" import { showSaveSuccessOrRestartToast } from "@/lib/restart-required" import { refreshGatewayState } from "@/store/gateway" import { AddModelSheet } from "./add-model-sheet" import { DeleteModelDialog } from "./delete-model-dialog" import { EditModelSheet } from "./edit-model-sheet" import { PROVIDER_PRIORITY, getProviderKey, getProviderLabel, } from "./provider-label" import { ProviderSection } from "./provider-section" interface ProviderGroup { key: string label: string models: ModelInfo[] hasDefault: boolean availableCount: number } export function ModelsPage() { const { t } = useTranslation() const [models, setModels] = useState([]) const [providerOptions, setProviderOptions] = useState( [], ) const [loading, setLoading] = useState(true) const [fetchError, setFetchError] = useState("") const [editingModel, setEditingModel] = useState(null) const [deletingModel, setDeletingModel] = useState(null) const [addOpen, setAddOpen] = useState(false) const [settingDefaultIndex, setSettingDefaultIndex] = useState( null, ) const addDisabled = loading || providerOptions.length === 0 const fetchModels = useCallback(async () => { try { const data = await getModels() const sorted = [...data.models].sort((a, b) => { if (a.is_default && !b.is_default) return -1 if (!a.is_default && b.is_default) return 1 if (a.available && !b.available) return -1 if (!a.available && b.available) return 1 return a.model_name.localeCompare(b.model_name) }) setModels(sorted) setProviderOptions(data.provider_options ?? []) setFetchError("") } catch (e) { setFetchError(e instanceof Error ? e.message : t("models.loadError")) } finally { setLoading(false) } }, [t]) useEffect(() => { fetchModels() }, [fetchModels]) const handleSetDefault = async (model: ModelInfo) => { if (model.is_default) return setSettingDefaultIndex(model.index) try { await setDefaultModel(model.model_name) await fetchModels() const gateway = await refreshGatewayState({ force: true }) showSaveSuccessOrRestartToast( t, t("models.defaultChangeSuccess"), model.model_name, gateway?.restartRequired === true, ) } catch (e) { toast.error(e instanceof Error ? e.message : t("models.loadError")) } finally { setSettingDefaultIndex(null) } } const grouped: Record = {} for (const model of models) { const providerKey = getProviderKey(model.provider) if (!grouped[providerKey]) { grouped[providerKey] = { label: getProviderLabel(model.provider), models: [], } } grouped[providerKey].models.push(model) } const providerGroups: ProviderGroup[] = Object.entries(grouped) .map(([key, group]) => { const availableCount = group.models.filter( (model) => model.available, ).length return { key, label: group.label, models: group.models, hasDefault: group.models.some((model) => model.is_default), availableCount, } }) .sort((a, b) => { if (a.hasDefault && !b.hasDefault) return -1 if (!a.hasDefault && b.hasDefault) return 1 if (a.availableCount !== b.availableCount) { return b.availableCount - a.availableCount } const aPriority = PROVIDER_PRIORITY[a.key] ?? Number.MAX_SAFE_INTEGER const bPriority = PROVIDER_PRIORITY[b.key] ?? Number.MAX_SAFE_INTEGER if (aPriority !== bPriority) { return aPriority - bPriority } return a.label.localeCompare(b.label) }) const defaultModel = models.find((model) => model.is_default) return (
{!defaultModel && (
{t("models.noDefaultHintPrefix")} {t("models.noDefaultHintSuffix")}
)}

{t("models.description")}

{loading && (
)} {fetchError && (
{fetchError}
)} {!loading && !fetchError && (
{providerGroups.map((providerGroup) => ( ))}
)}
setEditingModel(null)} onSaved={fetchModels} /> setAddOpen(false)} onSaved={fetchModels} existingModelNames={models.map((model) => model.model_name)} /> setDeletingModel(null)} onDeleted={fetchModels} />
) }