| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import { useState, useEffect, useContext, useRef, useMemo } from 'react'; |
| | import { useTranslation } from 'react-i18next'; |
| | import { API, copy, showError, showInfo, showSuccess } from '../../helpers'; |
| | import { Modal } from '@douyinfe/semi-ui'; |
| | import { UserContext } from '../../context/User'; |
| | import { StatusContext } from '../../context/Status'; |
| |
|
| | export const useModelPricingData = () => { |
| | const { t } = useTranslation(); |
| | const [searchValue, setSearchValue] = useState(''); |
| | const compositionRef = useRef({ isComposition: false }); |
| | const [selectedRowKeys, setSelectedRowKeys] = useState([]); |
| | const [modalImageUrl, setModalImageUrl] = useState(''); |
| | const [isModalOpenurl, setIsModalOpenurl] = useState(false); |
| | const [selectedGroup, setSelectedGroup] = useState('all'); |
| | const [showModelDetail, setShowModelDetail] = useState(false); |
| | const [selectedModel, setSelectedModel] = useState(null); |
| | const [filterGroup, setFilterGroup] = useState('all'); |
| | const [filterQuotaType, setFilterQuotaType] = useState('all'); |
| | const [filterEndpointType, setFilterEndpointType] = useState('all'); |
| | const [filterVendor, setFilterVendor] = useState('all'); |
| | const [filterTag, setFilterTag] = useState('all'); |
| | const [pageSize, setPageSize] = useState(20); |
| | const [currentPage, setCurrentPage] = useState(1); |
| | const [currency, setCurrency] = useState('USD'); |
| | const [showWithRecharge, setShowWithRecharge] = useState(false); |
| | const [tokenUnit, setTokenUnit] = useState('M'); |
| | const [models, setModels] = useState([]); |
| | const [vendorsMap, setVendorsMap] = useState({}); |
| | const [loading, setLoading] = useState(true); |
| | const [groupRatio, setGroupRatio] = useState({}); |
| | const [usableGroup, setUsableGroup] = useState({}); |
| | const [endpointMap, setEndpointMap] = useState({}); |
| | const [autoGroups, setAutoGroups] = useState([]); |
| |
|
| | const [statusState] = useContext(StatusContext); |
| | const [userState] = useContext(UserContext); |
| |
|
| | |
| | const priceRate = useMemo( |
| | () => statusState?.status?.price ?? 1, |
| | [statusState], |
| | ); |
| | const usdExchangeRate = useMemo( |
| | () => statusState?.status?.usd_exchange_rate ?? priceRate, |
| | [statusState, priceRate], |
| | ); |
| | const customExchangeRate = useMemo( |
| | () => statusState?.status?.custom_currency_exchange_rate ?? 1, |
| | [statusState], |
| | ); |
| | const customCurrencySymbol = useMemo( |
| | () => statusState?.status?.custom_currency_symbol ?? '¤', |
| | [statusState], |
| | ); |
| |
|
| | |
| | const siteDisplayType = useMemo( |
| | () => statusState?.status?.quota_display_type || 'USD', |
| | [statusState], |
| | ); |
| | useEffect(() => { |
| | if ( |
| | siteDisplayType === 'USD' || |
| | siteDisplayType === 'CNY' || |
| | siteDisplayType === 'CUSTOM' |
| | ) { |
| | setCurrency(siteDisplayType); |
| | } |
| | }, [siteDisplayType]); |
| |
|
| | const filteredModels = useMemo(() => { |
| | let result = models; |
| |
|
| | |
| | if (filterGroup !== 'all') { |
| | result = result.filter((model) => |
| | model.enable_groups.includes(filterGroup), |
| | ); |
| | } |
| |
|
| | |
| | if (filterQuotaType !== 'all') { |
| | result = result.filter((model) => model.quota_type === filterQuotaType); |
| | } |
| |
|
| | |
| | if (filterEndpointType !== 'all') { |
| | result = result.filter( |
| | (model) => |
| | model.supported_endpoint_types && |
| | model.supported_endpoint_types.includes(filterEndpointType), |
| | ); |
| | } |
| |
|
| | |
| | if (filterVendor !== 'all') { |
| | if (filterVendor === 'unknown') { |
| | result = result.filter((model) => !model.vendor_name); |
| | } else { |
| | result = result.filter((model) => model.vendor_name === filterVendor); |
| | } |
| | } |
| |
|
| | |
| | if (filterTag !== 'all') { |
| | const tagLower = filterTag.toLowerCase(); |
| | result = result.filter((model) => { |
| | if (!model.tags) return false; |
| | const tagsArr = model.tags |
| | .toLowerCase() |
| | .split(/[,;|]+/) |
| | .map((tag) => tag.trim()) |
| | .filter(Boolean); |
| | return tagsArr.includes(tagLower); |
| | }); |
| | } |
| |
|
| | |
| | if (searchValue.length > 0) { |
| | const searchTerm = searchValue.toLowerCase(); |
| | result = result.filter( |
| | (model) => |
| | (model.model_name && |
| | model.model_name.toLowerCase().includes(searchTerm)) || |
| | (model.description && |
| | model.description.toLowerCase().includes(searchTerm)) || |
| | (model.tags && model.tags.toLowerCase().includes(searchTerm)) || |
| | (model.vendor_name && |
| | model.vendor_name.toLowerCase().includes(searchTerm)), |
| | ); |
| | } |
| |
|
| | return result; |
| | }, [ |
| | models, |
| | searchValue, |
| | filterGroup, |
| | filterQuotaType, |
| | filterEndpointType, |
| | filterVendor, |
| | filterTag, |
| | ]); |
| |
|
| | const rowSelection = useMemo( |
| | () => ({ |
| | selectedRowKeys, |
| | onChange: (keys) => { |
| | setSelectedRowKeys(keys); |
| | }, |
| | }), |
| | [selectedRowKeys], |
| | ); |
| |
|
| | const displayPrice = (usdPrice) => { |
| | let priceInUSD = usdPrice; |
| | if (showWithRecharge) { |
| | priceInUSD = (usdPrice * priceRate) / usdExchangeRate; |
| | } |
| |
|
| | if (currency === 'CNY') { |
| | return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`; |
| | } else if (currency === 'CUSTOM') { |
| | return `${customCurrencySymbol}${(priceInUSD * customExchangeRate).toFixed(3)}`; |
| | } |
| | return `$${priceInUSD.toFixed(3)}`; |
| | }; |
| |
|
| | const setModelsFormat = (models, groupRatio, vendorMap) => { |
| | for (let i = 0; i < models.length; i++) { |
| | const m = models[i]; |
| | m.key = m.model_name; |
| | m.group_ratio = groupRatio[m.model_name]; |
| |
|
| | if (m.vendor_id && vendorMap[m.vendor_id]) { |
| | const vendor = vendorMap[m.vendor_id]; |
| | m.vendor_name = vendor.name; |
| | m.vendor_icon = vendor.icon; |
| | m.vendor_description = vendor.description; |
| | } |
| | } |
| | 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, |
| | vendors, |
| | group_ratio, |
| | usable_group, |
| | supported_endpoint, |
| | auto_groups, |
| | } = res.data; |
| | if (success) { |
| | setGroupRatio(group_ratio); |
| | setUsableGroup(usable_group); |
| | setSelectedGroup('all'); |
| | |
| | const vendorMap = {}; |
| | if (Array.isArray(vendors)) { |
| | vendors.forEach((v) => { |
| | vendorMap[v.id] = v; |
| | }); |
| | } |
| | setVendorsMap(vendorMap); |
| | setEndpointMap(supported_endpoint || {}); |
| | setAutoGroups(auto_groups || []); |
| | setModelsFormat(data, group_ratio, vendorMap); |
| | } 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 }); |
| | } |
| | }; |
| |
|
| | const handleChange = (value) => { |
| | const newSearchValue = value ? value : ''; |
| | setSearchValue(newSearchValue); |
| | }; |
| |
|
| | const handleCompositionStart = () => { |
| | compositionRef.current.isComposition = true; |
| | }; |
| |
|
| | const handleCompositionEnd = (event) => { |
| | compositionRef.current.isComposition = false; |
| | const value = event.target.value; |
| | const newSearchValue = value ? value : ''; |
| | setSearchValue(newSearchValue); |
| | }; |
| |
|
| | const handleGroupClick = (group) => { |
| | setSelectedGroup(group); |
| | setFilterGroup(group); |
| | if (group === 'all') { |
| | showInfo(t('已切换至最优倍率视图,每个模型使用其最低倍率分组')); |
| | } else { |
| | showInfo( |
| | t('当前查看的分组为:{{group}},倍率为:{{ratio}}', { |
| | group: group, |
| | ratio: groupRatio[group] ?? 1, |
| | }), |
| | ); |
| | } |
| | }; |
| |
|
| | const openModelDetail = (model) => { |
| | setSelectedModel(model); |
| | setShowModelDetail(true); |
| | }; |
| |
|
| | const closeModelDetail = () => { |
| | setShowModelDetail(false); |
| | setTimeout(() => { |
| | setSelectedModel(null); |
| | }, 300); |
| | }; |
| |
|
| | useEffect(() => { |
| | refresh().then(); |
| | }, []); |
| |
|
| | |
| | useEffect(() => { |
| | setCurrentPage(1); |
| | }, [ |
| | filterGroup, |
| | filterQuotaType, |
| | filterEndpointType, |
| | filterVendor, |
| | filterTag, |
| | searchValue, |
| | ]); |
| |
|
| | return { |
| | |
| | searchValue, |
| | setSearchValue, |
| | selectedRowKeys, |
| | setSelectedRowKeys, |
| | modalImageUrl, |
| | setModalImageUrl, |
| | isModalOpenurl, |
| | setIsModalOpenurl, |
| | selectedGroup, |
| | setSelectedGroup, |
| | showModelDetail, |
| | setShowModelDetail, |
| | selectedModel, |
| | setSelectedModel, |
| | filterGroup, |
| | setFilterGroup, |
| | filterQuotaType, |
| | setFilterQuotaType, |
| | filterEndpointType, |
| | setFilterEndpointType, |
| | filterVendor, |
| | setFilterVendor, |
| | filterTag, |
| | setFilterTag, |
| | pageSize, |
| | setPageSize, |
| | currentPage, |
| | setCurrentPage, |
| | currency, |
| | setCurrency, |
| | showWithRecharge, |
| | setShowWithRecharge, |
| | tokenUnit, |
| | setTokenUnit, |
| | models, |
| | loading, |
| | groupRatio, |
| | usableGroup, |
| | endpointMap, |
| | autoGroups, |
| |
|
| | |
| | priceRate, |
| | usdExchangeRate, |
| | filteredModels, |
| | rowSelection, |
| |
|
| | |
| | vendorsMap, |
| |
|
| | |
| | userState, |
| | statusState, |
| |
|
| | |
| | displayPrice, |
| | refresh, |
| | copyText, |
| | handleChange, |
| | handleCompositionStart, |
| | handleCompositionEnd, |
| | handleGroupClick, |
| | openModelDetail, |
| | closeModelDetail, |
| |
|
| | |
| | compositionRef, |
| |
|
| | |
| | t, |
| | }; |
| | }; |
| |
|