useModelPricingData.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useState, useEffect, useContext, useRef, useMemo } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
  18. import { Modal } from '@douyinfe/semi-ui';
  19. import { UserContext } from '../../context/User/index.js';
  20. import { StatusContext } from '../../context/Status/index.js';
  21. export const useModelPricingData = () => {
  22. const { t } = useTranslation();
  23. const [searchValue, setSearchValue] = useState('');
  24. const compositionRef = useRef({ isComposition: false });
  25. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  26. const [modalImageUrl, setModalImageUrl] = useState('');
  27. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  28. const [selectedGroup, setSelectedGroup] = useState('default');
  29. const [showModelDetail, setShowModelDetail] = useState(false);
  30. const [selectedModel, setSelectedModel] = useState(null);
  31. const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,"all" 表示不过滤
  32. const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
  33. const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
  34. const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
  35. const [pageSize, setPageSize] = useState(10);
  36. const [currentPage, setCurrentPage] = useState(1);
  37. const [currency, setCurrency] = useState('USD');
  38. const [showWithRecharge, setShowWithRecharge] = useState(false);
  39. const [tokenUnit, setTokenUnit] = useState('M');
  40. const [models, setModels] = useState([]);
  41. const [vendorsMap, setVendorsMap] = useState({});
  42. const [loading, setLoading] = useState(true);
  43. const [groupRatio, setGroupRatio] = useState({});
  44. const [usableGroup, setUsableGroup] = useState({});
  45. const [statusState] = useContext(StatusContext);
  46. const [userState] = useContext(UserContext);
  47. // 充值汇率(price)与美元兑人民币汇率(usd_exchange_rate)
  48. const priceRate = useMemo(() => statusState?.status?.price ?? 1, [statusState]);
  49. const usdExchangeRate = useMemo(() => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate]);
  50. const filteredModels = useMemo(() => {
  51. let result = models;
  52. // 分组筛选
  53. if (filterGroup !== 'all') {
  54. result = result.filter(model => model.enable_groups.includes(filterGroup));
  55. }
  56. // 计费类型筛选
  57. if (filterQuotaType !== 'all') {
  58. result = result.filter(model => model.quota_type === filterQuotaType);
  59. }
  60. // 端点类型筛选
  61. if (filterEndpointType !== 'all') {
  62. result = result.filter(model =>
  63. model.supported_endpoint_types &&
  64. model.supported_endpoint_types.includes(filterEndpointType)
  65. );
  66. }
  67. // 供应商筛选
  68. if (filterVendor !== 'all') {
  69. if (filterVendor === 'unknown') {
  70. result = result.filter(model => !model.vendor_name);
  71. } else {
  72. result = result.filter(model => model.vendor_name === filterVendor);
  73. }
  74. }
  75. // 搜索筛选
  76. if (searchValue.length > 0) {
  77. const searchTerm = searchValue.toLowerCase();
  78. result = result.filter(model =>
  79. (model.model_name && model.model_name.toLowerCase().includes(searchTerm)) ||
  80. (model.description && model.description.toLowerCase().includes(searchTerm)) ||
  81. (model.tags && model.tags.toLowerCase().includes(searchTerm)) ||
  82. (model.vendor_name && model.vendor_name.toLowerCase().includes(searchTerm))
  83. );
  84. }
  85. return result;
  86. }, [models, searchValue, filterGroup, filterQuotaType, filterEndpointType, filterVendor]);
  87. const rowSelection = useMemo(
  88. () => ({
  89. selectedRowKeys,
  90. onChange: (keys) => {
  91. setSelectedRowKeys(keys);
  92. },
  93. }),
  94. [selectedRowKeys],
  95. );
  96. const displayPrice = (usdPrice) => {
  97. let priceInUSD = usdPrice;
  98. if (showWithRecharge) {
  99. priceInUSD = usdPrice * priceRate / usdExchangeRate;
  100. }
  101. if (currency === 'CNY') {
  102. return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`;
  103. }
  104. return `$${priceInUSD.toFixed(3)}`;
  105. };
  106. const setModelsFormat = (models, groupRatio, vendorMap) => {
  107. for (let i = 0; i < models.length; i++) {
  108. const m = models[i];
  109. m.key = m.model_name;
  110. m.group_ratio = groupRatio[m.model_name];
  111. if (m.vendor_id && vendorMap[m.vendor_id]) {
  112. const vendor = vendorMap[m.vendor_id];
  113. m.vendor_name = vendor.name;
  114. m.vendor_icon = vendor.icon;
  115. m.vendor_description = vendor.description;
  116. }
  117. }
  118. models.sort((a, b) => {
  119. return a.quota_type - b.quota_type;
  120. });
  121. models.sort((a, b) => {
  122. if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
  123. return -1;
  124. } else if (
  125. !a.model_name.startsWith('gpt') &&
  126. b.model_name.startsWith('gpt')
  127. ) {
  128. return 1;
  129. } else {
  130. return a.model_name.localeCompare(b.model_name);
  131. }
  132. });
  133. setModels(models);
  134. };
  135. const loadPricing = async () => {
  136. setLoading(true);
  137. let url = '/api/pricing';
  138. const res = await API.get(url);
  139. const { success, message, data, vendors, group_ratio, usable_group } = res.data;
  140. if (success) {
  141. setGroupRatio(group_ratio);
  142. setUsableGroup(usable_group);
  143. setSelectedGroup(userState.user ? userState.user.group : 'default');
  144. // 构建供应商 Map 方便查找
  145. const vendorMap = {};
  146. if (Array.isArray(vendors)) {
  147. vendors.forEach(v => {
  148. vendorMap[v.id] = v;
  149. });
  150. }
  151. setVendorsMap(vendorMap);
  152. setModelsFormat(data, group_ratio, vendorMap);
  153. } else {
  154. showError(message);
  155. }
  156. setLoading(false);
  157. };
  158. const refresh = async () => {
  159. await loadPricing();
  160. };
  161. const copyText = async (text) => {
  162. if (await copy(text)) {
  163. showSuccess(t('已复制:') + text);
  164. } else {
  165. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  166. }
  167. };
  168. const handleChange = (value) => {
  169. if (compositionRef.current.isComposition) {
  170. return;
  171. }
  172. const newSearchValue = value ? value : '';
  173. setSearchValue(newSearchValue);
  174. };
  175. const handleCompositionStart = () => {
  176. compositionRef.current.isComposition = true;
  177. };
  178. const handleCompositionEnd = (event) => {
  179. compositionRef.current.isComposition = false;
  180. const value = event.target.value;
  181. const newSearchValue = value ? value : '';
  182. setSearchValue(newSearchValue);
  183. };
  184. const handleGroupClick = (group) => {
  185. setSelectedGroup(group);
  186. // 同时将分组过滤设置为该分组
  187. setFilterGroup(group);
  188. showInfo(
  189. t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
  190. group: group,
  191. ratio: groupRatio[group],
  192. }),
  193. );
  194. };
  195. const openModelDetail = (model) => {
  196. setSelectedModel(model);
  197. setShowModelDetail(true);
  198. };
  199. const closeModelDetail = () => {
  200. setShowModelDetail(false);
  201. setSelectedModel(null);
  202. };
  203. useEffect(() => {
  204. refresh().then();
  205. }, []);
  206. // 当筛选条件变化时重置到第一页
  207. useEffect(() => {
  208. setCurrentPage(1);
  209. }, [filterGroup, filterQuotaType, filterEndpointType, filterVendor, searchValue]);
  210. return {
  211. // 状态
  212. searchValue,
  213. setSearchValue,
  214. selectedRowKeys,
  215. setSelectedRowKeys,
  216. modalImageUrl,
  217. setModalImageUrl,
  218. isModalOpenurl,
  219. setIsModalOpenurl,
  220. selectedGroup,
  221. setSelectedGroup,
  222. showModelDetail,
  223. setShowModelDetail,
  224. selectedModel,
  225. setSelectedModel,
  226. filterGroup,
  227. setFilterGroup,
  228. filterQuotaType,
  229. setFilterQuotaType,
  230. filterEndpointType,
  231. setFilterEndpointType,
  232. filterVendor,
  233. setFilterVendor,
  234. pageSize,
  235. setPageSize,
  236. currentPage,
  237. setCurrentPage,
  238. currency,
  239. setCurrency,
  240. showWithRecharge,
  241. setShowWithRecharge,
  242. tokenUnit,
  243. setTokenUnit,
  244. models,
  245. loading,
  246. groupRatio,
  247. usableGroup,
  248. // 计算属性
  249. priceRate,
  250. usdExchangeRate,
  251. filteredModels,
  252. rowSelection,
  253. // 供应商
  254. vendorsMap,
  255. // 用户和状态
  256. userState,
  257. statusState,
  258. // 方法
  259. displayPrice,
  260. refresh,
  261. copyText,
  262. handleChange,
  263. handleCompositionStart,
  264. handleCompositionEnd,
  265. handleGroupClick,
  266. openModelDetail,
  267. closeModelDetail,
  268. // 引用
  269. compositionRef,
  270. // 国际化
  271. t,
  272. };
  273. };