/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useContext, useEffect, useRef, useMemo, useState } from 'react'; import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag, stringToColor } from '../../helpers'; import { useTranslation } from 'react-i18next'; import { Input, Layout, Modal, Space, Table, Tag, Tooltip, Popover, ImagePreview, Button, Card, Tabs, TabPane, Empty, Switch, Select } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IconVerify, IconHelpCircle, IconSearch, IconCopy, IconInfoCircle, IconLayers } from '@douyinfe/semi-icons'; import { UserContext } from '../../context/User/index.js'; import { AlertCircle } from 'lucide-react'; import { StatusContext } from '../../context/Status/index.js'; const ModelPricing = () => { const { t } = useTranslation(); const [filteredValue, setFilteredValue] = useState([]); const compositionRef = useRef({ isComposition: false }); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [modalImageUrl, setModalImageUrl] = useState(''); const [isModalOpenurl, setIsModalOpenurl] = useState(false); const [selectedGroup, setSelectedGroup] = useState('default'); const [activeKey, setActiveKey] = useState('all'); const [pageSize, setPageSize] = useState(10); const [currency, setCurrency] = useState('USD'); const [showWithRecharge, setShowWithRecharge] = useState(false); const [tokenUnit, setTokenUnit] = useState('M'); const [statusState] = useContext(StatusContext); // 充值汇率(price)与美元兑人民币汇率(usd_exchange_rate) const priceRate = useMemo(() => statusState?.status?.price ?? 1, [statusState]); const usdExchangeRate = useMemo(() => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate]); const rowSelection = useMemo( () => ({ onChange: (selectedRowKeys, selectedRows) => { setSelectedRowKeys(selectedRowKeys); }, }), [], ); const handleChange = (value) => { if (compositionRef.current.isComposition) { return; } const newFilteredValue = value ? [value] : []; setFilteredValue(newFilteredValue); }; const handleCompositionStart = () => { compositionRef.current.isComposition = true; }; const handleCompositionEnd = (event) => { compositionRef.current.isComposition = false; const value = event.target.value; const newFilteredValue = value ? [value] : []; setFilteredValue(newFilteredValue); }; function renderQuotaType(type) { switch (type) { case 1: return ( {t('按次计费')} ); case 0: return ( {t('按量计费')} ); default: return t('未知'); } } function renderAvailable(available) { return available ? ( {t('您的分组可以使用该模型')} } position='top' key={available} className="bg-green-50" > ) : null; } function renderSupportedEndpoints(endpoints) { if (!endpoints || endpoints.length === 0) { return null; } return ( {endpoints.map((endpoint, idx) => ( {endpoint} ))} ); } const displayPrice = (usdPrice) => { let priceInUSD = usdPrice; if (showWithRecharge) { priceInUSD = usdPrice * priceRate / usdExchangeRate; } if (currency === 'CNY') { return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`; } return `$${priceInUSD.toFixed(3)}`; }; const columns = [ { title: t('可用性'), dataIndex: 'available', render: (text, record, index) => { return renderAvailable(record.enable_groups.includes(selectedGroup)); }, sorter: (a, b) => { const aAvailable = a.enable_groups.includes(selectedGroup); const bAvailable = b.enable_groups.includes(selectedGroup); return Number(aAvailable) - Number(bAvailable); }, defaultSortOrder: 'descend', }, { title: t('可用端点类型'), dataIndex: 'supported_endpoint_types', render: (text, record, index) => { return renderSupportedEndpoints(text); }, }, { title: t('模型名称'), dataIndex: 'model_name', render: (text, record, index) => { return renderModelTag(text, { onClick: () => { copyText(text); } }); }, onFilter: (value, record) => record.model_name.toLowerCase().includes(value.toLowerCase()), filteredValue, }, { title: t('计费类型'), dataIndex: 'quota_type', render: (text, record, index) => { return renderQuotaType(parseInt(text)); }, sorter: (a, b) => a.quota_type - b.quota_type, }, { title: t('可用分组'), dataIndex: 'enable_groups', render: (text, record, index) => { return ( {text.map((group) => { if (usableGroup[group]) { if (group === selectedGroup) { return ( }> {group} ); } else { return ( { setSelectedGroup(group); showInfo( t('当前查看的分组为:{{group}},倍率为:{{ratio}}', { group: group, ratio: groupRatio[group], }), ); }} className="cursor-pointer hover:opacity-80 transition-opacity" > {group} ); } } })} ); }, }, { title: () => (
{t('倍率')} { setModalImageUrl('/ratio.png'); setIsModalOpenurl(true); }} />
), dataIndex: 'model_ratio', render: (text, record, index) => { let content = text; let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); content = (
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
{t('补全倍率')}: {record.quota_type === 0 ? completionRatio : t('无')}
{t('分组倍率')}:{groupRatio[selectedGroup]}
); return content; }, }, { title: (
{t('模型价格')} {/* 计费单位切换 */} setTokenUnit(checked ? 'K' : 'M')} checkedText="K" uncheckedText="M" />
), dataIndex: 'model_price', render: (text, record, index) => { let content = text; if (record.quota_type === 0) { let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; let completionRatioPriceUSD = record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; const unitDivisor = tokenUnit === 'K' ? 1000 : 1; const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; let displayInput = displayPrice(inputRatioPriceUSD); let displayCompletion = displayPrice(completionRatioPriceUSD); const divisor = unitDivisor; const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor; const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor; displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`; displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`; content = (
{t('提示')} {displayInput} / 1{unitLabel} tokens
{t('补全')} {displayCompletion} / 1{unitLabel} tokens
); } else { let priceUSD = parseFloat(text) * groupRatio[selectedGroup]; let displayVal = displayPrice(priceUSD); content = (
{t('模型价格')}:{displayVal}
); } return content; }, }, ]; const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [userState] = useContext(UserContext); const [groupRatio, setGroupRatio] = useState({}); const [usableGroup, setUsableGroup] = useState({}); const setModelsFormat = (models, groupRatio) => { for (let i = 0; i < models.length; i++) { models[i].key = models[i].model_name; models[i].group_ratio = groupRatio[models[i].model_name]; } models.sort((a, b) => { return a.quota_type - b.quota_type; }); models.sort((a, b) => { if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) { return -1; } else if ( !a.model_name.startsWith('gpt') && b.model_name.startsWith('gpt') ) { return 1; } else { return a.model_name.localeCompare(b.model_name); } }); setModels(models); }; const loadPricing = async () => { setLoading(true); let url = '/api/pricing'; const res = await API.get(url); const { success, message, data, group_ratio, usable_group } = res.data; if (success) { setGroupRatio(group_ratio); setUsableGroup(usable_group); setSelectedGroup(userState.user ? userState.user.group : 'default'); setModelsFormat(data, group_ratio); } else { showError(message); } setLoading(false); }; const refresh = async () => { await loadPricing(); }; const copyText = async (text) => { if (await copy(text)) { showSuccess(t('已复制:') + text); } else { Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); } }; useEffect(() => { refresh().then(); }, []); const modelCategories = getModelCategories(t); const categoryCounts = useMemo(() => { const counts = {}; if (models.length > 0) { counts['all'] = models.length; Object.entries(modelCategories).forEach(([key, category]) => { if (key !== 'all') { counts[key] = models.filter(model => category.filter(model)).length; } }); } return counts; }, [models, modelCategories]); const availableCategories = useMemo(() => { if (!models.length) return ['all']; return Object.entries(modelCategories).filter(([key, category]) => { if (key === 'all') return true; return models.some(model => category.filter(model)); }).map(([key]) => key); }, [models]); const renderTabs = () => { return ( setActiveKey(key)} className="mt-2" > {Object.entries(modelCategories) .filter(([key]) => availableCategories.includes(key)) .map(([key, category]) => { const modelCount = categoryCounts[key] || 0; return ( {category.icon && {category.icon}} {category.label} {modelCount} } itemKey={key} key={key} /> ); })} ); }; const filteredModels = useMemo(() => { let result = models; if (activeKey !== 'all') { result = result.filter(model => modelCategories[activeKey].filter(model)); } if (filteredValue.length > 0) { const searchTerm = filteredValue[0].toLowerCase(); result = result.filter(model => model.model_name.toLowerCase().includes(searchTerm) ); } return result; }, [activeKey, models, filteredValue]); const SearchAndActions = useMemo(() => (
} placeholder={t('模糊搜索模型名称')} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onChange={handleChange} showClear />
{/* 充值价格显示开关 */} {t('以充值价格显示')} {showWithRecharge && ( )}
), [selectedRowKeys, t, showWithRecharge, currency]); const ModelTable = useMemo(() => ( } darkModeImage={} description={t('搜索无结果')} style={{ padding: 30 }} /> } pagination={{ defaultPageSize: 10, pageSize: pageSize, showSizeChanger: true, pageSizeOptions: [10, 20, 50, 100], onPageSizeChange: (size) => setPageSize(size), }} /> ), [filteredModels, loading, columns, rowSelection, pageSize, t]); return (
{/* 主卡片容器 */} {/* 顶部状态卡片 */}
{t('模型定价')}
{userState.user ? (
{t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]}
) : (
{t('未登录,使用默认分组倍率:')}{groupRatio['default']}
)}
{t('分组倍率')}
{groupRatio[selectedGroup] || '1.0'}x
{t('可用模型')}
{models.filter(m => m.enable_groups.includes(selectedGroup)).length}
{t('计费类型')}
2
{/* 计费说明 */}
{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}
{/* 模型分类 Tabs */}
{renderTabs()} {/* 搜索和表格区域 */} {SearchAndActions} {ModelTable}
{/* 倍率说明图预览 */} setIsModalOpenurl(visible)} />
); }; export default ModelPricing;