import { IconLoader2, IconPlus, IconStar } from "@tabler/icons-react" import { useCallback, useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { type ModelInfo, getModels, setDefaultModel } from "@/api/models" import { PageHeader } from "@/components/page-header" import { Button } from "@/components/ui/button" import { AddModelSheet } from "./add-model-sheet" import { DeleteModelDialog } from "./delete-model-dialog" import { EditModelSheet } from "./edit-model-sheet" import { getProviderKey, getProviderLabel } from "./provider-label" import { ProviderSection } from "./provider-section" const PROVIDER_PRIORITY: Record = { volcengine: 0, openai: 1, gemini: 2, anthropic: 3, zhipu: 4, deepseek: 5, openrouter: 6, qwen: 7, moonshot: 8, groq: 9, "github-copilot": 10, antigravity: 11, nvidia: 12, cerebras: 13, shengsuanyun: 14, ollama: 15, vllm: 16, mistral: 17, avian: 18, mimo: 19, } interface ProviderGroup { key: string label: string models: ModelInfo[] hasDefault: boolean configuredCount: number } export function ModelsPage() { const { t } = useTranslation() const [models, setModels] = 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 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.configured && !b.configured) return -1 if (!a.configured && b.configured) return 1 return a.model_name.localeCompare(b.model_name) }) setModels(sorted) 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() } catch { // ignore } finally { setSettingDefaultIndex(null) } } const grouped: Record = {} for (const model of models) { const providerKey = getProviderKey(model.model) if (!grouped[providerKey]) { grouped[providerKey] = { label: getProviderLabel(model.model), models: [], } } grouped[providerKey].models.push(model) } const providerGroups: ProviderGroup[] = Object.entries(grouped) .map(([key, group]) => { const configuredCount = group.models.filter( (model) => model.configured, ).length return { key, label: group.label, models: group.models, hasDefault: group.models.some((model) => model.is_default), configuredCount, } }) .sort((a, b) => { if (a.hasDefault && !b.hasDefault) return -1 if (!a.hasDefault && b.hasDefault) return 1 if (a.configuredCount !== b.configuredCount) { return b.configuredCount - a.configuredCount } 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} />
) }