| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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,
|
| };
|
| };
|
|
|